#include "mednafen/mednafen.h"
#include "mednafen/mempatcher.h"
#include "mednafen/git.h"
#include "mednafen/general.h"
#include "mednafen/settings.h"
#include <compat/msvc.h>
#include "mednafen/psx/gpu.h"
#ifdef NEED_DEINTERLACER
#include "mednafen/video/Deinterlacer.h"
#endif
#include <libretro.h>
#include <rthreads/rthreads.h>
#include <streams/file_stream.h>
#include <string/stdstring.h>
#include <rhash.h>
#include "ugui_tools.h"
#include "rsx/rsx_intf.h"
#include "libretro_cbs.h"
#include "beetle_psx_globals.h"
#include "libretro_options.h"
#include "input.h"

#include "parallel-psx/custom-textures/dbg_input_callback.h"
retro_input_state_t dbg_input_state_cb = 0;

#include "mednafen/mednafen-endian.h"
#include "mednafen/mednafen-types.h"
#include "mednafen/psx/psx.h"
#include "mednafen/error.h"

#include "pgxp/pgxp_main.h"

#include <vector>
#define ISHEXDEC ((codeLine[cursor]>='0') && (codeLine[cursor]<='9')) || ((codeLine[cursor]>='a') && (codeLine[cursor]<='f')) || ((codeLine[cursor]>='A') && (codeLine[cursor]<='F'))

#ifdef HAVE_LIGHTREC
#include <sys/mman.h>

#ifdef HAVE_ASHMEM
#include <sys/ioctl.h>
#include <linux/ashmem.h>
#endif

#if defined(HAVE_SHM) || defined(HAVE_ASHMEM)
#include <sys/stat.h>
#include <fcntl.h>
#endif

#ifdef HAVE_WIN_SHM
#include <windows.h>
#endif
#endif /* HAVE_LIGHTREC */

//Fast Save States exclude string labels from variables in the savestate, and are at least 20% faster.
extern bool FastSaveStates;
const int DEFAULT_STATE_SIZE = 16 * 1024 * 1024;

static bool libretro_supports_bitmasks = false;
static unsigned libretro_msg_interface_version = 0;

struct retro_perf_callback perf_cb;
retro_get_cpu_features_t perf_get_cpu_features_cb = NULL;
retro_log_printf_t log_cb;
static retro_audio_sample_t audio_cb;
static retro_audio_sample_batch_t audio_batch_cb;
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;
static unsigned frame_count = 0;
static unsigned internal_frame_count = 0;
static bool display_internal_framerate = false;
static bool allow_frame_duping = false;
static bool failed_init = false;
static unsigned image_offset = 0;
static unsigned image_crop = 0;
static bool enable_memcard1 = false;
static bool enable_variable_serialization_size = false;
static int frame_width = 0;
static int frame_height = 0;
static bool gui_inited = false;
static bool gui_show = false;
static char bios_path[4096];
static bool firmware_found = false;

// Switchable memory cards
static int memcard_left_index = 0;
static int memcard_left_index_old;
static int memcard_right_index = 1;
static int memcard_right_index_old;

unsigned cd_2x_speedup = 1;
bool cd_async = false;
bool cd_warned_slow = false;
int64 cd_slow_timeout = 8000; // microseconds

// If true, PAL games will run at 60fps
bool fast_pal = false;

#ifdef HAVE_LIGHTREC
enum DYNAREC psx_dynarec;
bool psx_dynarec_invalidate;
uint8 psx_mmap = 0;
uint8 *psx_mem = NULL;
uint8 *psx_bios = NULL;
uint8 *psx_scratch = NULL;
#if defined(HAVE_ASHMEM)
int memfd;
#endif
#endif

uint32 EventCycles = 128;

// CPU overclock factor (or 0 if disabled)
int32_t psx_overclock_factor = 0;
// GPU rasterizer overclock shift
unsigned psx_gpu_overclock_shift = 0;

// Sets how often (in number of output frames/retro_run invocations)
// the internal framerace counter should be updated if
// display_internal_framerate is true.
#define INTERNAL_FPS_SAMPLE_PERIOD 64

static int psx_skipbios;

bool psx_gte_overclock;
enum dither_mode psx_gpu_dither_mode;

//iCB: PGXP options
unsigned int psx_pgxp_mode;
int psx_pgxp_2d_tol;
unsigned int psx_pgxp_vertex_caching;
unsigned int psx_pgxp_texture_correction;
unsigned int psx_pgxp_nclip;
// \iCB

#define NEGCON_RANGE 0x7FFF

char retro_save_directory[4096];
char retro_base_directory[4096];
char retro_cd_base_directory[4096];
static char retro_cd_path[4096];
char retro_cd_base_name[4096];
#ifdef _WIN32
   static char retro_slash = '\\';
#else
   static char retro_slash = '/';
#endif

enum
{
   REGION_JP = 0,
   REGION_NA = 1,
   REGION_EU = 2,
};

static bool firmware_is_present(unsigned region)
{
   static const size_t list_size = 10;
   const char *bios_name_list[list_size];
   const char *bios_sha1 = NULL;

   log_cb(RETRO_LOG_INFO, "Checking if required firmware is present...\n");

   /* SHA1 and alternate BIOS names sourced from
   https://github.com/mamedev/mame/blob/master/src/mame/drivers/psx.cpp */
   if (region == REGION_JP)
   {
      bios_name_list[0] = "scph5500.bin";
      bios_name_list[1] = "SCPH5500.bin";
      bios_name_list[2] = "SCPH-5500.bin";
      bios_name_list[3] = NULL;
      bios_name_list[4] = NULL;
      bios_name_list[5] = NULL;
      bios_name_list[6] = NULL;
      bios_name_list[7] = NULL;
      bios_name_list[8] = NULL;
      bios_name_list[9] = NULL;
      bios_sha1 = "B05DEF971D8EC59F346F2D9AC21FB742E3EB6917";
   }
   else if (region == REGION_NA)
   {
      bios_name_list[0] = "scph5501.bin";
      bios_name_list[1] = "SCPH5501.bin";
      bios_name_list[2] = "SCPH-5501.bin";
      bios_name_list[3] = "scph5503.bin";
      bios_name_list[4] = "SCPH5503.bin";
      bios_name_list[5] = "SCPH-5503.bin";
      bios_name_list[6] = "scph7003.bin";
      bios_name_list[7] = "SCPH7003.bin";
      bios_name_list[8] = "SCPH-7003.bin";
      bios_name_list[9] = NULL;
      bios_sha1 = "0555C6FAE8906F3F09BAF5988F00E55F88E9F30B";
   }
   else if (region == REGION_EU)
   {
      bios_name_list[0] = "scph5502.bin";
      bios_name_list[1] = "SCPH5502.bin";
      bios_name_list[2] = "SCPH-5502.bin";
      bios_name_list[3] = "scph5552.bin";
      bios_name_list[4] = "SCPH5552.bin";
      bios_name_list[5] = "SCPH-5552.bin";
      bios_name_list[6] = NULL;
      bios_name_list[7] = NULL;
      bios_name_list[8] = NULL;
      bios_name_list[9] = NULL;
      bios_sha1 = "F6BC2D1F5EB6593DE7D089C425AC681D6FFFD3F0";
   }

   size_t i;
   for (i = 0; i < list_size; ++i)
   {
      if (!bios_name_list[i])
         break;

      int r = snprintf(bios_path, sizeof(bios_path), "%s%c%s", retro_base_directory, retro_slash, bios_name_list[i]);
      if (r >= 4096)
      {
         log_cb(RETRO_LOG_ERROR, "Firmware path longer than 4095: %s\n", bios_path);
         break;
      }

      if (filestream_exists(bios_path))
      {
         firmware_found = true;
         break;
      }
   }

   if (!firmware_found)
   {
      char s[4096];

      log_cb(RETRO_LOG_ERROR, "Firmware is missing: %s\n", bios_name_list[0]);
      s[4095] = '\0';

      snprintf(s, sizeof(s), "Firmware is missing:\n\n%s", bios_name_list[0]);

      gui_set_message(s);
      gui_show = true;

      return false;
   }

   char obtained_sha1[41];
   sha1_calculate(bios_path, obtained_sha1);
   if (strcmp(obtained_sha1, bios_sha1))
   {
      log_cb(RETRO_LOG_WARN, "Firmware found but has invalid SHA1: %s\n", bios_path);
      log_cb(RETRO_LOG_WARN, "Expected SHA1: %s\n", bios_sha1);
      log_cb(RETRO_LOG_WARN, "Obtained SHA1: %s\n", obtained_sha1);
      log_cb(RETRO_LOG_WARN, "Unsupported firmware may cause emulation glitches.\n");
      return true;
   }

   log_cb(RETRO_LOG_INFO, "Firmware found: %s\n", bios_path);
   log_cb(RETRO_LOG_INFO, "Firmware SHA1: %s\n", obtained_sha1);

   return true;
}

static void extract_basename(char *buf, const char *path, size_t size)
{
   const char *base = strrchr(path, '/');
   if (!base)
      base = strrchr(path, '\\');
   if (!base)
      base = path;

   if (*base == '\\' || *base == '/')
      base++;

   strncpy(buf, base, size - strlen(buf) - 1);
   buf[size - 1] = '\0';

   char *ext = strrchr(buf, '.');
   if (ext)
      *ext = '\0';
}

static void extract_directory(char *buf, const char *path, size_t size)
{
   strncpy(buf, path, size - 1);
   buf[size - 1] = '\0';

   char *base = strrchr(buf, '/');
   if (!base)
      base = strrchr(buf, '\\');

   if (base)
      *base = '\0';
   else
      buf[0] = '\0';
}

/* start of Mednafen psx.cpp */

/* Mednafen - Multi-system Emulator
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "mednafen/psx/psx.h"
#include "mednafen/psx/mdec.h"
#include "mednafen/psx/frontio.h"
#include "mednafen/psx/timer.h"
#include "mednafen/psx/sio.h"
#include "mednafen/psx/cdc.h"
#include "mednafen/psx/spu.h"
#include "mednafen/mempatcher.h"

#include <stdarg.h>
#include <ctype.h>

bool setting_apply_analog_toggle  = false;
bool use_mednafen_memcard0_method = false;

extern MDFNGI EmulatedPSX;

#if PSX_DBGPRINT_ENABLE
static unsigned psx_dbg_level = 0;

void PSX_DBG(unsigned level, const char *format, ...)
{
   if(psx_dbg_level >= level)
   {
      va_list ap;
      va_start(ap, format);
      vprintf(format, ap);
      va_end(ap);
   }
}
#else
static unsigned const psx_dbg_level = 0;
#endif

/* Based off(but not the same as) public-domain "JKISS" PRNG. */
struct MDFN_PseudoRNG
{
   uint32_t x,y,z,c;
   uint64_t lcgo;
};

static MDFN_PseudoRNG PSX_PRNG;

uint32_t PSX_GetRandU32(uint32_t mina, uint32_t maxa)
{
   uint32_t tmp;
   const uint32_t range_m1 = maxa - mina;
   uint32_t range_mask = range_m1;

   range_mask |= range_mask >> 1;
   range_mask |= range_mask >> 2;
   range_mask |= range_mask >> 4;
   range_mask |= range_mask >> 8;
   range_mask |= range_mask >> 16;

   do
   {
      uint64_t t = 4294584393ULL * PSX_PRNG.z + PSX_PRNG.c;

      PSX_PRNG.x = 314527869 * PSX_PRNG.x + 1234567;
      PSX_PRNG.y ^= PSX_PRNG.y << 5;
      PSX_PRNG.y ^= PSX_PRNG.y >> 7;
      PSX_PRNG.y ^= PSX_PRNG.y << 22;
      PSX_PRNG.c = t >> 32;
      PSX_PRNG.z = t;
      PSX_PRNG.lcgo = (19073486328125ULL * PSX_PRNG.lcgo) + 1;
      tmp = ((PSX_PRNG.x + PSX_PRNG.y + PSX_PRNG.z) ^ (PSX_PRNG.lcgo >> 16)) & range_mask;
   } while(tmp > range_m1);

   return(mina + tmp);
}

static std::vector<CDIF *> CDInterfaces;  // FIXME: Cleanup on error out.
static std::vector<CDIF*> *cdifs = NULL;
static std::vector<const char *> cdifs_scex_ids;

static bool eject_state;

static bool CD_TrayOpen;
int CD_SelectedDisc;     // -1 for no disc

static bool CD_IsPBP = false;
extern int PBP_DiscCount;
/* The global value PBP_DiscCount is set to
 * zero when loading single-disk PBP files.
 * We therefore have to maintain a separate
 * 'physical' disk count, otherwise the
 * frontend disk control interface will fail */
static int PBP_PhysicalDiscCount;

typedef struct
{
   unsigned initial_index;
   std::string initial_path;
   std::vector<std::string> image_paths;
   std::vector<std::string> image_labels;
} disk_control_ext_info_t;

static disk_control_ext_info_t disk_control_ext_info;

static uint64_t Memcard_PrevDC[8];
static int64_t Memcard_SaveDelay[8];

PS_CPU *PSX_CPU = NULL;
PS_SPU *PSX_SPU = NULL;
PS_CDC *PSX_CDC = NULL;
FrontIO *PSX_FIO = NULL;

MultiAccessSizeMem<512 * 1024, uint32, false> *BIOSROM = NULL;
MultiAccessSizeMem<65536, uint32, false> *PIOMem = NULL;
MultiAccessSizeMem<2048 * 1024, uint32, false> *MainRAM = NULL;
MultiAccessSizeMem<1024, uint32, false> *ScratchRAM = NULL;

#ifdef HAVE_LIGHTREC
/* Size of Expansion 1 (8MB) */
#define PSX_EXPANSION1_SIZE        0x800000U
/* Base address of Expansion 1 */
#define PSX_EXPANSION1_BASE        0x1F000000U

/* Mednafen splits the expansion in two buffers (PIOMem and TextMem). That's not
 * super convenient for us so I'm going to copy both of them in one contiguous
 * buffer */
const uint8_t *PSX_LoadExpansion1(void) {
   static uint8_t *expansion1 = NULL;

   if (expansion1 == NULL) {
      expansion1 = new uint8_t[PSX_EXPANSION1_SIZE];
   }

   /* Let's read 32bits at a time to speed things up a bit */
   uint32_t *p = reinterpret_cast<uint32_t *>(expansion1);

   for (unsigned i = 0; i < PSX_EXPANSION1_SIZE / 4; i++) {
      p[i] = PSX_MemPeek32(PSX_EXPANSION1_BASE + i * 4);
   }

   return expansion1;
}
#endif

static uint32_t TextMem_Start;
static std::vector<uint8> TextMem;

static const uint32_t SysControl_Mask[9] = { 0x00ffffff, 0x00ffffff, 0xffffffff, 0x2f1fffff,
                                             0xffffffff, 0x2f1fffff, 0x2f1fffff, 0xffffffff,
                                             0x0003ffff };

static const uint32_t SysControl_OR[9] = { 0x1f000000, 0x1f000000, 0x00000000, 0x00000000,
                                           0x00000000, 0x00000000, 0x00000000, 0x00000000,
                                           0x00000000 };

static struct
{
   union
   {
      struct
      {
         uint32_t PIO_Base;   // 0x1f801000  // BIOS Init: 0x1f000000, Writeable bits: 0x00ffffff(assumed, verify), FixedOR = 0x1f000000
         uint32_t Unknown0;   // 0x1f801004  // BIOS Init: 0x1f802000, Writeable bits: 0x00ffffff, FixedOR = 0x1f000000
         uint32_t Unknown1;   // 0x1f801008  // BIOS Init: 0x0013243f, ????
         uint32_t Unknown2;   // 0x1f80100c  // BIOS Init: 0x00003022, Writeable bits: 0x2f1fffff, FixedOR = 0x00000000

         uint32_t BIOS_Mapping;  // 0x1f801010  // BIOS Init: 0x0013243f, ????
         uint32_t SPU_Delay;  // 0x1f801014  // BIOS Init: 0x200931e1, Writeable bits: 0x2f1fffff, FixedOR = 0x00000000 - Affects bus timing on access to SPU
         uint32_t CDC_Delay;  // 0x1f801018  // BIOS Init: 0x00020843, Writeable bits: 0x2f1fffff, FixedOR = 0x00000000
         uint32_t Unknown4;   // 0x1f80101c  // BIOS Init: 0x00070777, ????
         uint32_t Unknown5;   // 0x1f801020  // BIOS Init: 0x00031125(but rewritten with other values often), Writeable bits: 0x0003ffff, FixedOR = 0x00000000 -- Possibly CDC related
      };
      uint32_t Regs[9];
   };
} SysControl;

static unsigned DMACycleSteal = 0;   // Doesn't need to be saved in save states, since it's calculated in the ForceEventUpdates() call chain.

void PSX_SetDMACycleSteal(unsigned stealage)
{
   if (stealage > 200) // Due to 8-bit limitations in the CPU core.
      stealage = 200;

   DMACycleSteal = stealage;
}

//
// Event stuff
//

static int32_t Running; // Set to -1 when not desiring exit, and 0 when we are.

struct event_list_entry
{
   uint32_t which;
   int32_t event_time;
   event_list_entry *prev;
   event_list_entry *next;
};

static event_list_entry events[PSX_EVENT__COUNT];

static void EventReset(void)
{
   unsigned i;
   for(i = 0; i < PSX_EVENT__COUNT; i++)
   {
      events[i].which = i;

      if(i == PSX_EVENT__SYNFIRST)
        events[i].event_time = (int32_t)0x80000000;
      else if(i == PSX_EVENT__SYNLAST)
         events[i].event_time = 0x7FFFFFFF;
      else
         events[i].event_time = PSX_EVENT_MAXTS;

      events[i].prev = (i > 0) ? &events[i - 1] : NULL;
      events[i].next = (i < (PSX_EVENT__COUNT - 1)) ? &events[i + 1] : NULL;
   }
}

//static void RemoveEvent(event_list_entry *e)
//{
// e->prev->next = e->next;
// e->next->prev = e->prev;
//}

static void RebaseTS(const int32_t timestamp)
{
   unsigned i;
   for(i = 0; i < PSX_EVENT__COUNT; i++)
   {
      if(i == PSX_EVENT__SYNFIRST || i == PSX_EVENT__SYNLAST)
         continue;

      assert(events[i].event_time > timestamp);
      events[i].event_time -= timestamp;
   }

   PSX_CPU->SetEventNT(events[PSX_EVENT__SYNFIRST].next->event_time);
}

void PSX_SetEventNT(const int type, const int32_t next_timestamp)
{
   event_list_entry *e = &events[type];

   if(next_timestamp < e->event_time)
   {
      event_list_entry *fe = e;

      do
      {
         fe = fe->prev;
      }while(next_timestamp < fe->event_time);

      // Remove this event from the list, temporarily of course.
      e->prev->next = e->next;
      e->next->prev = e->prev;

      // Insert into the list, just after "fe".
      e->prev = fe;
      e->next = fe->next;
      fe->next->prev = e;
      fe->next = e;

      e->event_time = next_timestamp;
   }
   else if(next_timestamp > e->event_time)
   {
      event_list_entry *fe = e;

      do
      {
         fe = fe->next;
      } while(next_timestamp > fe->event_time);

      // Remove this event from the list, temporarily of course
      e->prev->next = e->next;
      e->next->prev = e->prev;

      // Insert into the list, just BEFORE "fe".
      e->prev = fe->prev;
      e->next = fe;
      fe->prev->next = e;
      fe->prev = e;

      e->event_time = next_timestamp;
   }

   PSX_CPU->SetEventNT(events[PSX_EVENT__SYNFIRST].next->event_time & Running);
}

// Called from debug.cpp too.
void ForceEventUpdates(const int32_t timestamp)
{
   PSX_SetEventNT(PSX_EVENT_GPU, GPU_Update(timestamp));
   PSX_SetEventNT(PSX_EVENT_CDC, PSX_CDC->Update(timestamp));

   PSX_SetEventNT(PSX_EVENT_TIMER, TIMER_Update(timestamp));

   PSX_SetEventNT(PSX_EVENT_DMA, DMA_Update(timestamp));

   PSX_SetEventNT(PSX_EVENT_FIO, PSX_FIO->Update(timestamp));

   PSX_CPU->SetEventNT(events[PSX_EVENT__SYNFIRST].next->event_time);
}

bool MDFN_FASTCALL PSX_EventHandler(const int32_t timestamp)
{
   event_list_entry *e = events[PSX_EVENT__SYNFIRST].next;

   while(timestamp >= e->event_time)   // If Running = 0, PSX_EventHandler() may be called even if there isn't an event per-se, so while() instead of do { ... } while
   {
      int32_t nt;
      event_list_entry *prev = e->prev;

      switch(e->which)
      {
         default:
            abort();
         case PSX_EVENT_GPU:
            nt = GPU_Update(e->event_time);
            break;
         case PSX_EVENT_CDC:
            nt = PSX_CDC->Update(e->event_time);
            break;
         case PSX_EVENT_TIMER:
            nt = TIMER_Update(e->event_time);
            break;
         case PSX_EVENT_DMA:
            nt = DMA_Update(e->event_time);
            break;
         case PSX_EVENT_FIO:
            nt = PSX_FIO->Update(e->event_time);
            break;
      }

      PSX_SetEventNT(e->which, nt);

      // Order of events can change due to calling PSX_SetEventNT(), this prev business ensures we don't miss an event due to reordering.
      e = prev->next;
   }

   return(Running);
}


void PSX_RequestMLExit(void)
{
   Running = 0;
   PSX_CPU->SetEventNT(0);
}


//
// End event stuff
//


/* Remember to update MemPeek<>() and MemPoke<>() when we change address decoding in MemRW() */
template<typename T, bool IsWrite, bool Access24> static INLINE void MemRW(int32_t &timestamp, uint32_t A, uint32_t &V)
{
#if 0
   if(IsWrite)
      printf("Write%d: %08x(orig=%08x), %08x\n", (int)(sizeof(T) * 8), A & mask[A >> 29], A, V);
   else
      printf("Read%d: %08x(orig=%08x)\n", (int)(sizeof(T) * 8), A & mask[A >> 29], A);
#endif

   if(!IsWrite)
      timestamp += DMACycleSteal;

   //if(A == 0xa0 && IsWrite)
   // DBG_Break();

   if(A < 0x00800000)
   {
      if(IsWrite)
      {
         //timestamp++; // Best-case timing.
      }
      else
      {
         // Overclock: get rid of memory access latency
         if (!psx_gte_overclock)
            timestamp += 3;
      }

      if(Access24)
      {
         if(IsWrite)
            MainRAM->WriteU24(A & 0x1FFFFF, V);
         else
            V = MainRAM->ReadU24(A & 0x1FFFFF);
      }
      else
      {
         if(IsWrite)
            MainRAM->Write<T>(A & 0x1FFFFF, V);
         else
            V = MainRAM->Read<T>(A & 0x1FFFFF);
      }

      return;
   }

   if(A >= 0x1FC00000 && A <= 0x1FC7FFFF)
   {
      if(!IsWrite)
      {
         if(Access24)
            V = BIOSROM->ReadU24(A & 0x7FFFF);
         else
            V = BIOSROM->Read<T>(A & 0x7FFFF);
      }

      return;
   }

   if(timestamp >= events[PSX_EVENT__SYNFIRST].next->event_time)
      PSX_EventHandler(timestamp);

   if(A >= 0x1F801000 && A <= 0x1F802FFF)
   {

      //if(IsWrite)
      // printf("HW Write%d: %08x %08x\n", (unsigned int)(sizeof(T)*8), (unsigned int)A, (unsigned int)V);
      //else
      // printf("HW Read%d: %08x\n", (unsigned int)(sizeof(T)*8), (unsigned int)A);

      if(A >= 0x1F801C00 && A <= 0x1F801FFF) // SPU
      {
         if(sizeof(T) == 4 && !Access24)
         {
            if(IsWrite)
            {
               //timestamp += 15;

               //if(timestamp >= events[PSX_EVENT__SYNFIRST].next->event_time)
               // PSX_EventHandler(timestamp);

               PSX_SPU->Write(timestamp, A | 0, V);
               PSX_SPU->Write(timestamp, A | 2, V >> 16);
            }
            else
            {
               timestamp += 36;

               if(timestamp >= events[PSX_EVENT__SYNFIRST].next->event_time)
                  PSX_EventHandler(timestamp);

               V = PSX_SPU->Read(timestamp, A) | (PSX_SPU->Read(timestamp, A | 2) << 16);
            }
         }
         else
         {
            if(IsWrite)
            {
               //timestamp += 8;

               //if(timestamp >= events[PSX_EVENT__SYNFIRST].next->event_time)
               // PSX_EventHandler(timestamp);

               PSX_SPU->Write(timestamp, A & ~1, V);
            }
            else
            {
               timestamp += 16; // Just a guess, need to test.

               if(timestamp >= events[PSX_EVENT__SYNFIRST].next->event_time)
                  PSX_EventHandler(timestamp);

               V = PSX_SPU->Read(timestamp, A & ~1);
            }
         }
         return;
      }     // End SPU


      // CDC: TODO - 8-bit access.
      if(A >= 0x1f801800 && A <= 0x1f80180F)
      {
         if(!IsWrite)
         {
            timestamp += 6 * sizeof(T); //24;
         }

         if(IsWrite)
            PSX_CDC->Write(timestamp, A & 0x3, V);
         else
            V = PSX_CDC->Read(timestamp, A & 0x3);

         return;
      }

      if(A >= 0x1F801810 && A <= 0x1F801817)
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            GPU_Write(timestamp, A, V);
         else
            V = GPU_Read(timestamp, A);

         return;
      }

      if(A >= 0x1F801820 && A <= 0x1F801827)
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            MDEC_Write(timestamp, A, V);
         else
            V = MDEC_Read(timestamp, A);

         return;
      }

      if(A >= 0x1F801000 && A <= 0x1F801023)
      {
         unsigned index = (A & 0x1F) >> 2;

         if(!IsWrite)
            timestamp++;

         //if(A == 0x1F801014 && IsWrite)
         // fprintf(stderr, "%08x %08x\n",A,V);

         if(IsWrite)
         {
            V <<= (A & 3) * 8;
            SysControl.Regs[index] = V & SysControl_Mask[index];
         }
         else
         {
            V = SysControl.Regs[index] | SysControl_OR[index];
            V >>= (A & 3) * 8;
         }
         return;
      }

      if(A >= 0x1F801040 && A <= 0x1F80104F)
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            PSX_FIO->Write(timestamp, A, V);
         else
            V = PSX_FIO->Read(timestamp, A);
         return;
      }

      if(A >= 0x1F801050 && A <= 0x1F80105F)
      {
         if(!IsWrite)
            timestamp++;

#if 0
         if(IsWrite)
         {
            PSX_WARNING("[SIO] Write: 0x%08x 0x%08x %u", A, V, (unsigned)sizeof(T));
         }
         else
         {
            PSX_WARNING("[SIO] Read: 0x%08x", A);
         }
#endif

         if(IsWrite)
            SIO_Write(timestamp, A, V);
         else
            V = SIO_Read(timestamp, A);
         return;
      }

#if 0
      if(A >= 0x1F801060 && A <= 0x1F801063)
      {
         if(IsWrite)
         {

         }
         else
         {

         }

         return;
      }
#endif

      if(A >= 0x1F801070 && A <= 0x1F801077) // IRQ
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            ::IRQ_Write(A, V);
         else
            V = ::IRQ_Read(A);
         return;
      }

      if(A >= 0x1F801080 && A <= 0x1F8010FF)    // DMA
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            DMA_Write(timestamp, A, V);
         else
            V = DMA_Read(timestamp, A);

         return;
      }

      if(A >= 0x1F801100 && A <= 0x1F80113F) // Root counters
      {
         if(!IsWrite)
            timestamp++;

         if(IsWrite)
            TIMER_Write(timestamp, A, V);
         else
            V = TIMER_Read(timestamp, A);

         return;
      }
   }


   if(A >= 0x1F000000 && A <= 0x1F7FFFFF)
   {
      if(!IsWrite)
      {
         //if((A & 0x7FFFFF) <= 0x84)
         //PSX_WARNING("[PIO] Read%d from 0x%08x at time %d", (int)(sizeof(T) * 8), A, timestamp);

         V = ~0U; // A game this affects:  Tetris with Cardcaptor Sakura

         if(PIOMem)
         {
            if((A & 0x7FFFFF) < 65536)
            {
               if(Access24)
                  V = PIOMem->ReadU24(A & 0x7FFFFF);
               else
                  V = PIOMem->Read<T>(A & 0x7FFFFF);
            }
            else if((A & 0x7FFFFF) < (65536 + TextMem.size()))
            {
               if(Access24)
                  V = MDFN_de24lsb(&TextMem[(A & 0x7FFFFF) - 65536]);
               else switch(sizeof(T))
               {
                  case 1: V = TextMem[(A & 0x7FFFFF) - 65536]; break;
                  case 2: V = MDFN_de16lsb<false>(&TextMem[(A & 0x7FFFFF) - 65536]); break;
                  case 4: V = MDFN_de32lsb<false>(&TextMem[(A & 0x7FFFFF) - 65536]); break;
               }
            }
         }
      }
      return;
   }

   if(A == 0xFFFE0130) // Per tests on PS1, ignores the access(sort of, on reads the value is forced to 0 if not aligned) if not aligned to 4-bytes.
   {
      if(!IsWrite)
         V = PSX_CPU->GetBIU();
      else
         PSX_CPU->SetBIU(V);

      return;
   }

   if(IsWrite)
   {
      PSX_WARNING("[MEM] Unknown write%d to %08x at time %d, =%08x(%d)", (int)(sizeof(T) * 8), A, timestamp, V, V);
   }
   else
   {
      V = 0;
      PSX_WARNING("[MEM] Unknown read%d from %08x at time %d", (int)(sizeof(T) * 8), A, timestamp);
   }
}

void MDFN_FASTCALL PSX_MemWrite8(int32_t timestamp, uint32_t A, uint32_t V)
{
   MemRW<uint8, true, false>(timestamp, A, V);
}

void MDFN_FASTCALL PSX_MemWrite16(int32_t timestamp, uint32_t A, uint32_t V)
{
   MemRW<uint16, true, false>(timestamp, A, V);
}

void MDFN_FASTCALL PSX_MemWrite24(int32_t timestamp, uint32_t A, uint32_t V)
{
   MemRW<uint32, true, true>(timestamp, A, V);
}

void MDFN_FASTCALL PSX_MemWrite32(int32_t timestamp, uint32_t A, uint32_t V)
{
   MemRW<uint32, true, false>(timestamp, A, V);
}

uint8_t MDFN_FASTCALL PSX_MemRead8(int32_t &timestamp, uint32_t A)
{
   uint32_t V;

   MemRW<uint8, false, false>(timestamp, A, V);

   return(V);
}

uint16_t MDFN_FASTCALL PSX_MemRead16(int32_t &timestamp, uint32_t A)
{
   uint32_t V;

   MemRW<uint16, false, false>(timestamp, A, V);

   return(V);
}

uint32_t MDFN_FASTCALL PSX_MemRead24(int32_t &timestamp, uint32_t A)
{
   uint32_t V;

   MemRW<uint32, false, true>(timestamp, A, V);

   return(V);
}

uint32_t MDFN_FASTCALL PSX_MemRead32(int32_t &timestamp, uint32_t A)
{
   uint32_t V;

   MemRW<uint32, false, false>(timestamp, A, V);

   return(V);
}

template<typename T, bool Access24> static INLINE uint32_t MemPeek(int32_t timestamp, uint32_t A)
{
   if(A < 0x00800000)
   {
      if(Access24)
         return(MainRAM->ReadU24(A & 0x1FFFFF));
      return(MainRAM->Read<T>(A & 0x1FFFFF));
   }

   if(A >= 0x1FC00000 && A <= 0x1FC7FFFF)
   {
      if(Access24)
         return(BIOSROM->ReadU24(A & 0x7FFFF));
      return(BIOSROM->Read<T>(A & 0x7FFFF));
   }

   if(A >= 0x1F801000 && A <= 0x1F802FFF)
   {
      if(A >= 0x1F801C00 && A <= 0x1F801FFF) // SPU
      {
         // TODO

      }     // End SPU


      // CDC: TODO - 8-bit access.
      if(A >= 0x1f801800 && A <= 0x1f80180F)
      {
         // TODO

      }

      if(A >= 0x1F801810 && A <= 0x1F801817)
      {
         // TODO

      }

      if(A >= 0x1F801820 && A <= 0x1F801827)
      {
         // TODO

      }

      if(A >= 0x1F801000 && A <= 0x1F801023)
      {
         unsigned index = (A & 0x1F) >> 2;
         return((SysControl.Regs[index] | SysControl_OR[index]) >> ((A & 3) * 8));
      }

      if(A >= 0x1F801040 && A <= 0x1F80104F)
      {
         // TODO

      }

      if(A >= 0x1F801050 && A <= 0x1F80105F)
      {
         // TODO

      }


      if(A >= 0x1F801070 && A <= 0x1F801077) // IRQ
      {
         // TODO

      }

      if(A >= 0x1F801080 && A <= 0x1F8010FF)    // DMA
      {
         // TODO

      }

      if(A >= 0x1F801100 && A <= 0x1F80113F) // Root counters
      {
         // TODO

      }
   }


   if(A >= 0x1F000000 && A <= 0x1F7FFFFF)
   {
      if(PIOMem)
      {
         if((A & 0x7FFFFF) < 65536)
         {
            if(Access24)
               return(PIOMem->ReadU24(A & 0x7FFFFF));
            return(PIOMem->Read<T>(A & 0x7FFFFF));
         }
         else if((A & 0x7FFFFF) < (65536 + TextMem.size()))
         {
            if(Access24)
               return(MDFN_de24lsb(&TextMem[(A & 0x7FFFFF) - 65536]));
            else switch(sizeof(T))
            {
               case 1:
                  return(TextMem[(A & 0x7FFFFF) - 65536]);
               case 2:
                  return(MDFN_de16lsb<false>(&TextMem[(A & 0x7FFFFF) - 65536]));
               case 4:
                  return(MDFN_de32lsb<false>(&TextMem[(A & 0x7FFFFF) - 65536]));
            }
         }
      }
      return(~0U);
   }

   if(A == 0xFFFE0130)
      return PSX_CPU->GetBIU();

   return(0);
}

uint8_t PSX_MemPeek8(uint32_t A)
{
   return MemPeek<uint8, false>(0, A);
}

uint16_t PSX_MemPeek16(uint32_t A)
{
   return MemPeek<uint16, false>(0, A);
}

uint32_t PSX_MemPeek32(uint32_t A)
{
   return MemPeek<uint32, false>(0, A);
}

// FIXME: Add PSX_Reset() and FrontIO::Reset() so that emulated input devices don't get power-reset on reset-button reset.
static void PSX_Power(void)
{
   unsigned i;

   PSX_PRNG.x = 123456789;
   PSX_PRNG.y = 987654321;
   PSX_PRNG.z = 43219876;
   PSX_PRNG.c = 6543217;
   PSX_PRNG.lcgo = 0xDEADBEEFCAFEBABEULL;

   cd_warned_slow = false;

   memset(MainRAM->data32, 0, 2048 * 1024);

   for(i = 0; i < 9; i++)
      SysControl.Regs[i] = 0;

   PSX_CPU->Power();

   EventReset();

   TIMER_Power();

   DMA_Power();

   PSX_FIO->Power();
   SIO_Power();

   MDEC_Power();
   PSX_CDC->Power();
   GPU_Power();
   //SPU->Power();   // Called from CDC->Power()
   IRQ_Power();

   ForceEventUpdates(0);
}

template<typename T, bool Access24> static INLINE void MemPoke(pscpu_timestamp_t timestamp, uint32 A, T V)
{
   if(A < 0x00800000)
   {
      if(Access24)
         MainRAM->WriteU24(A & 0x1FFFFF, V);
      else
         MainRAM->Write<T>(A & 0x1FFFFF, V);

      return;
   }

   if(A >= 0x1FC00000 && A <= 0x1FC7FFFF)
   {
      if(Access24)
         BIOSROM->WriteU24(A & 0x7FFFF, V);
      else
         BIOSROM->Write<T>(A & 0x7FFFF, V);

      return;
   }

   if(A >= 0x1F801000 && A <= 0x1F802FFF)
   {
      if(A >= 0x1F801000 && A <= 0x1F801023)
      {
         unsigned index = (A & 0x1F) >> 2;
         SysControl.Regs[index] = (V << ((A & 3) * 8)) & SysControl_Mask[index];
         return;
      }
   }

   if(A == 0xFFFE0130)
   {
      PSX_CPU->SetBIU(V);
      return;
   }
}

void PSX_MemPoke8(uint32 A, uint8 V)
{
   MemPoke<uint8, false>(0, A, V);
}

void PSX_MemPoke16(uint32 A, uint16 V)
{
   MemPoke<uint16, false>(0, A, V);
}

void PSX_MemPoke32(uint32 A, uint32 V)
{
   MemPoke<uint32, false>(0, A, V);
}

void PSX_GPULineHook(const int32_t timestamp, const int32_t line_timestamp, bool vsync, uint32_t *pixels, const MDFN_PixelFormat* const format, const unsigned width, const unsigned pix_clock_offset, const unsigned pix_clock, const unsigned pix_clock_divider, const unsigned surf_pitchinpix, const unsigned upscale_factor)
{
   PSX_FIO->GPULineHook(timestamp, line_timestamp, vsync, pixels, format, width, pix_clock_offset, pix_clock, pix_clock_divider, surf_pitchinpix, upscale_factor);
}

static bool TestMagic(const char *name, RFILE *fp, int64_t size)
{
   uint8_t header[8];

   if (size < 0x800)
      return(false);

   filestream_read(fp, header, 8);

   if (
         (header[0] == 'P') &&
         (header[1] == 'S') &&
         (header[2] == '-') &&
         (header[3] == 'X') &&
         (header[4] == ' ') &&
         (header[5] == 'E') &&
         (header[6] == 'X') &&
         (header[7] == 'E')
      )
      return(true);

   return(true);
}

static bool TestMagicCD(std::vector<CDIF *> *_CDInterfaces)
{
   uint8_t buf[2048];
   TOC toc;
   int dt;

   TOC_Clear(&toc);

   (*_CDInterfaces)[0]->ReadTOC(&toc);

   dt = TOC_FindTrackByLBA(&toc, 4);

   if(dt > 0 && !(toc.tracks[dt].control & 0x4))
      return(false);

   if((*_CDInterfaces)[0]->ReadSector(buf, 4, 1) != 0x2)
      return(false);

   if(strncmp((char *)buf + 10, "Licensed  by", strlen("Licensed  by")))
      return(false);

   //if(strncmp((char *)buf + 32, "Sony", 4))
   // return(false);

   //for(int i = 0; i < 2048; i++)
   // printf("%d, %02x %c\n", i, buf[i], buf[i]);
   //exit(1);

#if 0
   {
      uint8_t buf[2048 * 7];

      if((*cdifs)[0]->ReadSector(buf, 5, 7) == 0x2)
      {
         printf("CRC32: 0x%08x\n", (uint32)crc32(0, &buf[0], 0x3278));
      }
   }
#endif

   return(true);
}

static const char *CalcDiscSCEx_BySYSTEMCNF(CDIF *c, unsigned *rr)
{
   uint8_t pvd[2048];
   uint32_t rdel, rdel_len;
   const char *ret = NULL;
   Stream *fp      = NULL;
   unsigned pvd_search_count = 0;

   fp = c->MakeStream(0, ~0U);
   fp->seek(0x8000, SEEK_SET);

   try // Added this back to fix audio-cd loading issue
   {
   do
   {
      if((pvd_search_count++) == 32)
      {
         log_cb(RETRO_LOG_ERROR, "PVD search count limit met.\n");
         ret = NULL;
         goto Breakout;
      }

      fp->read(pvd, 2048);

      if(memcmp(&pvd[1], "CD001", 5))
      {
         log_cb(RETRO_LOG_ERROR, "Not ISO-9660\n");
         ret = NULL;
         goto Breakout;
      }

      if(pvd[0] == 0xFF)
      {
         log_cb(RETRO_LOG_ERROR, "Missing Primary Volume Descriptor\n");
         ret = NULL;
         goto Breakout;
      }
   } while(pvd[0] != 0x01);

   /*[156 ... 189], 34 bytes */
   rdel = MDFN_de32lsb<false>(&pvd[0x9E]);
   rdel_len = MDFN_de32lsb<false>(&pvd[0xA6]);

   if(rdel_len >= (1024 * 1024 * 10))  /* Arbitrary sanity check. */
   {
      log_cb(RETRO_LOG_ERROR, "Root directory table too large\n");
      ret = NULL;
      goto Breakout;
   }

   fp->seek((int64)rdel * 2048, SEEK_SET);
   //printf("%08x, %08x\n", rdel * 2048, rdel_len);
   while(fp->tell() < (((int64)rdel * 2048) + rdel_len))
   {
      uint8_t len_dr = fp->get_u8();
      uint8_t dr[256 + 1];

      memset(dr, 0xFF, sizeof(dr));

      if(!len_dr)
         break;

      memset(dr, 0, sizeof(dr));
      dr[0] = len_dr;
      fp->read(dr + 1, len_dr - 1);

      uint8_t len_fi = dr[0x20];

      if(len_fi == 12 && !memcmp(&dr[0x21], "SYSTEM.CNF;1", 12))
      {
         uint32_t file_lba = MDFN_de32lsb<false>(&dr[0x02]);
         //uint32_t file_len = MDFN_de32lsb<false>(&dr[0x0A]);
         uint8_t fb[2048 + 1];
         char *bootpos;

         memset(fb, 0, sizeof(fb));
         fp->seek(file_lba * 2048, SEEK_SET);
         fp->read(fb, 2048);

         bootpos = strstr((char*)fb, "BOOT") + 4;
         while(*bootpos == ' ' || *bootpos == '\t') bootpos++;
         if(*bootpos == '=')
         {
            bootpos++;
            while(*bootpos == ' ' || *bootpos == '\t') bootpos++;
            if(!strncasecmp(bootpos, "cdrom:\\", 7))
            {
               bootpos += 7;
               char *tmp;

               if((tmp = strchr(bootpos, '_'))) *tmp = 0;
               if((tmp = strchr(bootpos, '.'))) *tmp = 0;
               if((tmp = strchr(bootpos, ';'))) *tmp = 0;
               //puts(bootpos);

               if(strlen(bootpos) == 4 && bootpos[0] == 'S' && (bootpos[1] == 'C' || bootpos[1] == 'L' || bootpos[1] == 'I'))
               {
                  switch(bootpos[2])
                  {
                     case 'E': if(rr)
                                  *rr = REGION_EU;
                               ret = "SCEE";
                               goto Breakout;

                     case 'U': if(rr)
                                  *rr = REGION_NA;
                               ret = "SCEA";
                               goto Breakout;

                     case 'K':   // Korea?
                     case 'B':
                     case 'P': if(rr)
                                  *rr = REGION_JP;
                               ret = "SCEI";
                               goto Breakout;
                  }
               }
            }
         }

         //puts((char*)fb);
         //puts("ASOFKOASDFKO");
      }
   }
   } // try
   catch(std::exception &e)
   {
      //
   }
      catch(...)
   {
   }
       

Breakout:
   if(fp)
   {
      delete fp;
      fp = NULL;
   }

   return(ret);
}

static unsigned CalcDiscSCEx(void)
{
   const char *prev_valid_id = NULL;
   unsigned ret_region = MDFN_GetSettingI("psx.region_default");

   cdifs_scex_ids.clear();

   if(cdifs)
      for(unsigned i = 0; i < cdifs->size(); i++)
      {
         uint8_t buf[2048];
         uint8_t fbuf[2048 + 1];
         const char *id = CalcDiscSCEx_BySYSTEMCNF((*cdifs)[i], (i == 0) ? &ret_region : NULL);

         memset(fbuf, 0, sizeof(fbuf));

         if(id == NULL && (*cdifs)[i]->ReadSector(buf, 4, 1) == 0x2)
         {
            unsigned ipos, opos;
            for(ipos = 0, opos = 0; ipos < 0x48; ipos++)
            {
               if(buf[ipos] > 0x20 && buf[ipos] < 0x80)
               {
                  fbuf[opos++] = tolower(buf[ipos]);
               }
            }

            fbuf[opos++] = 0;

            PSX_DBG(PSX_DBG_SPARSE, "License string: %s", (char *)fbuf);

            if(strstr((char *)fbuf, "licensedby") != NULL)
            {
               if(strstr((char *)fbuf, "america") != NULL)
               {
                  id = "SCEA";
                  if(!i)
                     ret_region = REGION_NA;
               }
               else if(strstr((char *)fbuf, "europe") != NULL)
               {
                  id = "SCEE";
                  if(!i)
                     ret_region = REGION_EU;
               }
               else if(strstr((char *)fbuf, "japan") != NULL)
               {
                  id = "SCEI";   // ?
                  if(!i)
                     ret_region = REGION_JP;
               }
               else if(strstr((char *)fbuf, "sonycomputerentertainmentinc.") != NULL)
               {
                  id = "SCEI";
                  if(!i)
                     ret_region = REGION_JP;
               }
               else  // Failure case
               {
                  if(prev_valid_id != NULL)
                     id = prev_valid_id;
                  else
                  {
                     switch(ret_region)   // Less than correct, but meh, what can we do.
                     {
                        case REGION_JP:
                           id = "SCEI";
                           break;

                        case REGION_NA:
                           id = "SCEA";
                           break;

                        case REGION_EU:
                           id = "SCEE";
                           break;
                     }
                  }
               }
            }
         }

         if(id != NULL)
            prev_valid_id = id;

         cdifs_scex_ids.push_back(id);
      }

   return ret_region;
}

static void SetDiscWrapper(const bool CD_TrayOpen) {
    CDIF *cdif = NULL;
    const char *disc_id = NULL;
    if (CD_SelectedDisc >= 0 && !CD_TrayOpen) {
        // only allow one pbp file to be loaded (at index 0)
        if (CD_IsPBP) {
            cdif = (*cdifs)[0];
            disc_id = cdifs_scex_ids[0];
        } else {
            cdif = (*cdifs)[CD_SelectedDisc];
            disc_id = cdifs_scex_ids[CD_SelectedDisc];
        }
    }

    PSX_CDC->SetDisc(CD_TrayOpen, cdif, disc_id);
}

#ifdef HAVE_LIGHTREC
/* MAP_FIXED_NOREPLACE allows base 0 to work if "sysctl vm.mmap_min_addr = 0"
 was used. Base 0 will perform better by directly mapping emulated addresses
 to host addresses. If MAP_FIXED_NOREPLACE is not available we should not use
 MAP_FIXED, since it can cause strange crashes by unmapping memory mappings. */
#ifndef MAP_FIXED_NOREPLACE
#ifdef USE_FIXED
#define MAP_FIXED_NOREPLACE MAP_FIXED
#else
#define MAP_FIXED_NOREPLACE 0
#endif
#endif

static const uintptr_t supported_io_bases[] = {
	static_cast<uintptr_t>(0x00000000),
	static_cast<uintptr_t>(0x10000000),
	static_cast<uintptr_t>(0x20000000),
	static_cast<uintptr_t>(0x30000000),
	static_cast<uintptr_t>(0x40000000),
	static_cast<uintptr_t>(0x50000000),
	static_cast<uintptr_t>(0x60000000),
	static_cast<uintptr_t>(0x70000000),
	static_cast<uintptr_t>(0x80000000),
	static_cast<uintptr_t>(0x90000000),
   /* Some platforms need higher address base for mmap to work */
#if UINTPTR_MAX == UINT64_MAX
	static_cast<uintptr_t>(0x100000000),
	static_cast<uintptr_t>(0x200000000),
	static_cast<uintptr_t>(0x300000000),
	static_cast<uintptr_t>(0x400000000),
	static_cast<uintptr_t>(0x500000000),
	static_cast<uintptr_t>(0x600000000),
	static_cast<uintptr_t>(0x700000000),
	static_cast<uintptr_t>(0x800000000),
	static_cast<uintptr_t>(0x900000000),
#endif
};

#define RAM_SIZE 0x200000
#define BIOS_SIZE 0x80000
#define SCRATCH_SIZE 0x400

#ifdef HAVE_WIN_SHM
#define MAP(addr, size, fd, offset) \
	MapViewOfFileEx(fd, FILE_MAP_ALL_ACCESS, 0, offset, size, addr)
#define UNMAP(addr, size) UnmapViewOfFile(addr)
#define MFAILED NULL
#define NUM_MEM 4
#elif defined(HAVE_SHM) || defined(HAVE_ASHMEM)
#define MAP(addr, size, fd, offset) \
	mmap(addr,size, PROT_READ | PROT_WRITE, \
		MAP_SHARED | MAP_FIXED_NOREPLACE, fd, offset)
#define UNMAP(addr, size) munmap(addr, size)
#define MFAILED MAP_FAILED
#define NUM_MEM 4
#else
#define MAP(addr, size, fd, offset) \
	mmap(addr,size, PROT_READ | PROT_WRITE, \
		MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)
#define UNMAP(addr, size) munmap(addr, size)
#define MFAILED MAP_FAILED
#define NUM_MEM 1
#endif

int lightrec_init_mmap()
{
	int r = 0, i, j;
	uintptr_t base;
	void *bios, *scratch, *map;

/* open memfd and set size */
#ifdef HAVE_ASHMEM
	memfd = open("/dev/ashmem", O_RDWR);

	if (memfd < 0) {
		log_cb(RETRO_LOG_ERROR, "Failed to create ASHMEM: %s\n", strerror(errno));
		return 0;
	}

	ioctl(memfd, ASHMEM_SET_NAME, "lightrec_memfd");
	ioctl(memfd, ASHMEM_SET_SIZE, RAM_SIZE+BIOS_SIZE+SCRATCH_SIZE);
#endif
#ifdef HAVE_SHM
	int memfd;
	const char *shm_name = "/lightrec_memfd";

	memfd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL,
			S_IRUSR | S_IWUSR);

	if (memfd < 0) {
		log_cb(RETRO_LOG_ERROR, "Failed to create SHM: %s\n", strerror(errno));
		return 0;
	}

	/* unlink ASAP to prevent leaving a file in shared memory if we crash */
	shm_unlink(shm_name);

	if (ftruncate(memfd, RAM_SIZE+BIOS_SIZE+SCRATCH_SIZE) < 0) {
		log_cb(RETRO_LOG_ERROR, "Could not truncate SHM size: %s\n", strerror(errno));
		goto close_return;
	}
#endif
#ifdef HAVE_WIN_SHM
	HANDLE memfd;

	memfd = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0,
			RAM_SIZE+BIOS_SIZE+SCRATCH_SIZE, NULL);

	if (memfd == NULL) {
		log_cb(RETRO_LOG_ERROR, "Failed to create WIN_SHM: %s (%d)\n", strerror(errno), GetLastError());
		return 0;
	}
#endif

	/* Try to map at various base addresses*/
	for (i = 0; i < ARRAY_SIZE(supported_io_bases); i++) {
		base = supported_io_bases[i];
		bios = (void *)(base + 0x1fc00000);
		scratch = (void *)(base + 0x1f800000);

		for (j = 0; j < NUM_MEM; j++) {
			map = MAP((void *)(base + j * RAM_SIZE), RAM_SIZE, memfd, 0);
			if (map == MFAILED)
				break;
			else if (map != (void *)(base + j * RAM_SIZE))
			{
				//not at expected address, reject it
				UNMAP(map, RAM_SIZE);
				break;
			}
		}

		/* Impossible to map using this base */
		if (j == 0)
			continue;

		/* All mirrors mapped - we got a match! */
		if (j == NUM_MEM)
		{
			psx_mem = (uint8 *)base;

			map = MAP(bios, BIOS_SIZE, memfd, RAM_SIZE);
			if (map == MFAILED)
				goto err_unmap;

			psx_bios = (uint8 *)map;

			if (map != bios)
				goto err_unmap_bios;

			map = MAP(scratch, SCRATCH_SIZE, memfd, RAM_SIZE+BIOS_SIZE);
			if (map == MFAILED)
				goto err_unmap_bios;

			psx_scratch = (uint8 *)map;

			if (map != scratch)
				goto err_unmap_scratch;

			r = NUM_MEM;

			goto close_return;
		}

err_unmap_scratch:
		if(psx_scratch){
			UNMAP(psx_scratch, SCRATCH_SIZE);
			psx_scratch = NULL;
		}
err_unmap_bios:
		if(psx_bios){
			UNMAP(psx_bios, BIOS_SIZE);
			psx_bios = NULL;
		}
err_unmap:
		/* Clean up any mapped ram or mirrors and try again */
		for (; j > 0; j--)
			UNMAP((void *)(base + (j - 1) * RAM_SIZE), RAM_SIZE);

		psx_mem = NULL;
	}

	if (i == ARRAY_SIZE(supported_io_bases)) {
		log_cb(RETRO_LOG_WARN, "Unable to mmap on any base address, dynarec will be slower\n");
	}

close_return:
#ifdef HAVE_SHM
	close(memfd);
#endif
#ifdef HAVE_WIN_SHM
	CloseHandle(memfd);
#endif
	return r;
}

void lightrec_free_mmap()
{
	for (int i = 0; i < NUM_MEM; i++)
		UNMAP((void *)((uintptr_t)psx_mem + i * RAM_SIZE), RAM_SIZE);

	UNMAP(psx_bios, BIOS_SIZE);
	UNMAP(psx_scratch, SCRATCH_SIZE);

#ifdef HAVE_ASHMEM
	/* ashmem shared memory is not pinned by mmap, it dies on close */
	close(memfd);
#endif
}
#endif /* HAVE_LIGHTREC */

/* Forward declarations, required for disk control
 * 'set initial disk' functionality */
static unsigned disk_get_num_images(void);
static void CDInsertEject(void);
static void CDEject(void);

static void InitCommon(std::vector<CDIF *> *_CDInterfaces, const bool EmulateMemcards = true, const bool WantPIOMem = false)
{
   unsigned region, i;
   bool emulate_memcard[8];
   bool emulate_multitap[2];
   int sls, sle;

#if PSX_DBGPRINT_ENABLE
   psx_dbg_level = MDFN_GetSettingUI("psx.dbg_level");
#endif

   for(i = 0; i < 8; i++)
   {
      char buf[64];
      snprintf(buf, sizeof(buf), "psx.input.port%u.memcard", i + 1);
      emulate_memcard[i] = EmulateMemcards && MDFN_GetSettingB(buf);
   }

   if (!enable_memcard1) {
     emulate_memcard[1] = false;
   }

   emulate_multitap[0] = setting_psx_multitap_port_1;
   emulate_multitap[1] = setting_psx_multitap_port_2;

   cdifs  = _CDInterfaces;
   region = CalcDiscSCEx();

   if(!MDFN_GetSettingB("psx.region_autodetect"))
      region = MDFN_GetSettingI("psx.region_default");

   sls = MDFN_GetSettingI((region == REGION_EU) ? "psx.slstartp" : "psx.slstart");
   sle = MDFN_GetSettingI((region == REGION_EU) ? "psx.slendp" : "psx.slend");

   if(sls > sle)
   {
      int tmp = sls;
      sls = sle;
      sle = tmp;
   }

   PSX_CPU = new PS_CPU();
   PSX_SPU = new PS_SPU();

   GPU_Init(region == REGION_EU, sls, sle, psx_gpu_upscale_shift);

   PSX_CDC = new PS_CDC();
   PSX_FIO = new FrontIO(emulate_memcard, emulate_multitap);
   PSX_FIO->SetAMCT(MDFN_GetSettingB("psx.input.analog_mode_ct"));
   for(unsigned i = 0; i < 8; i++)
   {
      char buf[64];
      snprintf(buf, sizeof(buf), "psx.input.port%u.gun_chairs", i + 1);
      PSX_FIO->SetCrosshairsColor(i, MDFN_GetSettingUI(buf));
   }

	input_set_fio( PSX_FIO );

   DMA_Init();

   GPU_FillVideoParams(&EmulatedPSX);

   switch (psx_gpu_dither_mode)
   {
      case DITHER_NATIVE:
         GPU_set_dither_upscale_shift(psx_gpu_upscale_shift);
         break;
      case DITHER_UPSCALED:
         GPU_set_dither_upscale_shift(0);
         break;
      case DITHER_OFF:
         break;
   }

   PGXP_SetModes(psx_pgxp_mode | psx_pgxp_vertex_caching | psx_pgxp_texture_correction | psx_pgxp_nclip);

   CD_TrayOpen        = true;
   CD_SelectedDisc    = -1;

   if(cdifs)
   {
      CD_TrayOpen     = false;
      CD_SelectedDisc = 0;

      /* Attempt to set initial disk index */
      if ((disk_control_ext_info.initial_index > 0) &&
          (disk_control_ext_info.initial_index < disk_get_num_images()))
         if (disk_control_ext_info.initial_index <
               disk_control_ext_info.image_paths.size())
            if (string_is_equal(
                  disk_control_ext_info.image_paths[disk_control_ext_info.initial_index].c_str(),
                  disk_control_ext_info.initial_path.c_str()))
               CD_SelectedDisc = (int)disk_control_ext_info.initial_index;
   }

   PSX_CDC->SetDisc(true, NULL, NULL);

   /* Multi-disk PBP files cause additional complication
    * here, since the first disk is always loaded by default */
   if(CD_IsPBP && (CD_SelectedDisc > 0))
   {
      CDEject();
      CDInsertEject();
   }
   else
      SetDiscWrapper(CD_TrayOpen);

#ifdef HAVE_LIGHTREC
   psx_mmap = lightrec_init_mmap();

   if(psx_mmap > 0)
   {
      MainRAM = new(psx_mem) MultiAccessSizeMem<RAM_SIZE, uint32, false>();
      ScratchRAM = new(psx_scratch) MultiAccessSizeMem<SCRATCH_SIZE, uint32, false>();
      BIOSROM = new(psx_bios) MultiAccessSizeMem<BIOS_SIZE, uint32, false>();
   }
   else
#endif
   {
      MainRAM = new MultiAccessSizeMem<2048 * 1024, uint32, false>();
      ScratchRAM = new MultiAccessSizeMem<1024, uint32, false>();
      BIOSROM = new MultiAccessSizeMem<512 * 1024, uint32, false>();
   }

   PIOMem  = NULL;

   if(WantPIOMem)
      PIOMem = new MultiAccessSizeMem<65536, uint32, false>();

   for(uint32_t ma = 0x00000000; ma < 0x00800000; ma += 2048 * 1024)
   {
      PSX_CPU->SetFastMap(MainRAM->data32, 0x00000000 + ma, 2048 * 1024);
      PSX_CPU->SetFastMap(MainRAM->data32, 0x80000000 + ma, 2048 * 1024);
      PSX_CPU->SetFastMap(MainRAM->data32, 0xA0000000 + ma, 2048 * 1024);
   }

   PSX_CPU->SetFastMap(BIOSROM->data32, 0x1FC00000, 512 * 1024);
   PSX_CPU->SetFastMap(BIOSROM->data32, 0x9FC00000, 512 * 1024);
   PSX_CPU->SetFastMap(BIOSROM->data32, 0xBFC00000, 512 * 1024);

   if(PIOMem)
   {
      PSX_CPU->SetFastMap(PIOMem->data32, 0x1F000000, 65536);
      PSX_CPU->SetFastMap(PIOMem->data32, 0x9F000000, 65536);
      PSX_CPU->SetFastMap(PIOMem->data32, 0xBF000000, 65536);
   }


   MDFNMP_Init(1024, ((uint64)1 << 29) / 1024);
   MDFNMP_AddRAM(2048 * 1024, 0x00000000, MainRAM->data8);
#if 0
   MDFNMP_AddRAM(1024, 0x1F800000, ScratchRAM.data8);
#endif

   RFILE *BIOSFile;

   if(firmware_is_present(region))
   {
      BIOSFile      = filestream_open(bios_path,
            RETRO_VFS_FILE_ACCESS_READ,
            RETRO_VFS_FILE_ACCESS_HINT_NONE);
   }
   else
   {
      const char *biospath_sname;

      if(region == REGION_JP)
         biospath_sname = "psx.bios_jp";
      else if(region == REGION_EU)
         biospath_sname = "psx.bios_eu";
      else if(region == REGION_NA)
         biospath_sname = "psx.bios_na";
      else
         abort();

      const char *biospath = MDFN_MakeFName(MDFNMKF_FIRMWARE,
            0, MDFN_GetSettingS(biospath_sname));

      BIOSFile      = filestream_open(biospath,
            RETRO_VFS_FILE_ACCESS_READ,
            RETRO_VFS_FILE_ACCESS_HINT_NONE);
   }

      if (BIOSFile)
      {
         filestream_read(BIOSFile, BIOSROM->data8, 512 * 1024);
         filestream_close(BIOSFile);
      }

   i = 0;

   if (!use_mednafen_memcard0_method)
   {
      PSX_FIO->LoadMemcard(0);
      i = 1;
   }

   for(; i < 8; i++)
   {
      char ext[64];
      const char *memcard = NULL;
      if (i == 0)
         snprintf(ext, sizeof(ext), "%d.mcr", memcard_left_index);
      else if (i == 1)
         snprintf(ext, sizeof(ext), "%d.mcr", memcard_right_index);
      else
         snprintf(ext, sizeof(ext), "%d.mcr", i);
      memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
      PSX_FIO->LoadMemcard(i, memcard);
   }

   for(i = 0; i < 8; i++)
   {
      Memcard_PrevDC[i] = PSX_FIO->GetMemcardDirtyCount(i);
      Memcard_SaveDelay[i] = -1;
   }

	input_init_calibration();

#ifdef WANT_DEBUGGER
   DBG_Init();
#endif
   PSX_Power();
}

static bool LoadEXE(const uint8_t *data, const uint32_t size, bool ignore_pcsp = false)
{
   uint32 PC        = MDFN_de32lsb<false>(&data[0x10]);
   uint32 SP        = MDFN_de32lsb<false>(&data[0x30]);
   uint32 TextStart = MDFN_de32lsb<false>(&data[0x18]);
   uint32 TextSize  = MDFN_de32lsb<false>(&data[0x1C]);

   if(ignore_pcsp)
      log_cb(RETRO_LOG_DEBUG, "TextStart=0x%08x\nTextSize=0x%08x\n", TextStart, TextSize);
   else
      log_cb(RETRO_LOG_DEBUG, "PC=0x%08x\nSP=0x%08x\nTextStart=0x%08x\nTextSize=0x%08x\n", PC, SP, TextStart, TextSize);

   TextStart &= 0x1FFFFF;

   if(TextSize > 2048 * 1024)
   {
      MDFN_Error(0, "Text section too large");
      return false;
   }

   if(TextSize > (size - 0x800))
   {
      MDFN_Error(0, "Text section recorded size is larger than data available in file.  Header=0x%08x, Available=0x%08x", TextSize, size - 0x800);
      return false;
   }

   if(TextSize < (size - 0x800))
   {
      MDFN_Error(0, "Text section recorded size is smaller than data available in file.  Header=0x%08x, Available=0x%08x", TextSize, size - 0x800);
      return false;
   }

   if(!TextMem.size())
   {
      TextMem_Start = TextStart;
      TextMem.resize(TextSize);
   }

   if(TextStart < TextMem_Start)
   {
      uint32 old_size = TextMem.size();

      //printf("RESIZE: 0x%08x\n", TextMem_Start - TextStart);

      TextMem.resize(old_size + TextMem_Start - TextStart);
      memmove(&TextMem[TextMem_Start - TextStart], &TextMem[0], old_size);

      TextMem_Start = TextStart;
   }

   if(TextMem.size() < (TextStart - TextMem_Start + TextSize))
      TextMem.resize(TextStart - TextMem_Start + TextSize);

   memcpy(&TextMem[TextStart - TextMem_Start], data + 0x800, TextSize);

   // BIOS patch
   BIOSROM->WriteU32(0x6990, (3 << 26) | ((0xBF001000 >> 2) & ((1 << 26) - 1)));
#if 0
   BIOSROM->WriteU32(0x691C, (3 << 26) | ((0xBF001000 >> 2) & ((1 << 26) - 1)));
#endif

   uint8 *po;

   po = &PIOMem->data8[0x0800];

   MDFN_en32lsb<false>(po, (0x0 << 26) | (31 << 21) | (0x8 << 0)); // JR
   po += 4;
   MDFN_en32lsb<false>(po, 0); // NOP(kinda)
   po += 4;

   po = &PIOMem->data8[0x1000];

   // Load cacheable-region target PC into r2
   MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16) | (0x9F001010 >> 16));      // LUI
   po += 4;
   MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (2 << 16) | (0x9F001010 & 0xFFFF));   // ORI
   po += 4;

   // Jump to r2
   MDFN_en32lsb<false>(po, (0x0 << 26) | (2 << 21) | (0x8 << 0));  // JR
   po += 4;
   MDFN_en32lsb<false>(po, 0); // NOP(kinda)
   po += 4;

   //
   // 0x9F001010:
   //

   // Load source address into r8
   uint32 sa = 0x9F000000 + 65536;
   MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16) | (sa >> 16));  // LUI
   po += 4;
   MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (8 << 16) | (sa & 0xFFFF));  // ORI
   po += 4;

   // Load dest address into r9
   MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16)  | (TextMem_Start >> 16));  // LUI
   po += 4;
   MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (9 << 16) | (TextMem_Start & 0xFFFF));   // ORI
   po += 4;

   // Load size into r10
   MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16)  | (TextMem.size() >> 16)); // LUI
   po += 4;
   MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (10 << 16) | (TextMem.size() & 0xFFFF));    // ORI
   po += 4;

   //
   // Loop begin
   //

   MDFN_en32lsb<false>(po, (0x24 << 26) | (8 << 21) | (1 << 16));  // LBU to r1
   po += 4;

   MDFN_en32lsb<false>(po, (0x08 << 26) | (10 << 21) | (10 << 16) | 0xFFFF);   // Decrement size
   po += 4;

   MDFN_en32lsb<false>(po, (0x28 << 26) | (9 << 21) | (1 << 16));  // SB from r1
   po += 4;

   MDFN_en32lsb<false>(po, (0x08 << 26) | (8 << 21) | (8 << 16) | 0x0001);  // Increment source addr
   po += 4;

   MDFN_en32lsb<false>(po, (0x05 << 26) | (0 << 21) | (10 << 16) | (-5 & 0xFFFF));
   po += 4;
   MDFN_en32lsb<false>(po, (0x08 << 26) | (9 << 21) | (9 << 16) | 0x0001);  // Increment dest addr
   po += 4;

   //
   // Loop end
   //

   // Load SP into r29
   if(ignore_pcsp)
   {
      po += 16;
   }
   else
   {
      MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16)  | (SP >> 16)); // LUI
      po += 4;
      MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (29 << 16) | (SP & 0xFFFF));    // ORI
      po += 4;

      // Load PC into r2
      MDFN_en32lsb<false>(po, (0xF << 26) | (0 << 21) | (1 << 16)  | ((PC >> 16) | 0x8000));      // LUI
      po += 4;
      MDFN_en32lsb<false>(po, (0xD << 26) | (1 << 21) | (2 << 16) | (PC & 0xFFFF));   // ORI
      po += 4;
   }

   // Half-assed instruction cache flush. ;)
   for(unsigned i = 0; i < 1024; i++)
   {
      MDFN_en32lsb<false>(po, 0);
      po += 4;
   }



   // Jump to r2
   MDFN_en32lsb<false>(po, (0x0 << 26) | (2 << 21) | (0x8 << 0));  // JR
   po += 4;
   MDFN_en32lsb<false>(po, 0); // NOP(kinda)
   po += 4;

#ifdef HAVE_LIGHTREC
   /* Reload Expansion1 copy */
   PSX_LoadExpansion1();
#endif

   return true;
}

static int Load(const char *name, RFILE *fp)
{
   int64_t size     = filestream_get_size(fp);
   const bool IsPSF = false;
   char image_label[4096];

   image_label[0] = '\0';

   if(!TestMagic(name, fp, size))
   {
      MDFN_Error(0, "File format is unknown to module psx..");
      return -1;
   }

   InitCommon(NULL, !IsPSF, true);

   TextMem.resize(0);

   if(size >= 0x800)
   {
      int64_t len     = size;
      uint8_t *header = (uint8_t*)malloc(len * sizeof(uint8_t));

      filestream_read_file(name, (void**)&header, &len);

      if (!LoadEXE(header, len))
         return -1;

      free(header);
   }

   disk_control_ext_info.image_paths.push_back(name);
   extract_basename(image_label, name, sizeof(image_label));
   disk_control_ext_info.image_labels.push_back(image_label);

   return(1);
}

static int LoadCD(std::vector<CDIF *> *_CDInterfaces)
{
   InitCommon(_CDInterfaces);

   if (psx_skipbios == 1)
   BIOSROM->WriteU32(0x6990, 0);

   EmulatedPSX.GameType = GMT_CDROM;

   return(1);
}

static void Cleanup(void)
{
   TextMem.resize(0);

   if(PSX_CDC)
      delete PSX_CDC;
   PSX_CDC = NULL;

   if(PSX_SPU)
      delete PSX_SPU;
   PSX_SPU = NULL;

   GPU_Destroy();

   if(PSX_CPU)
      delete PSX_CPU;
   PSX_CPU = NULL;

   if(PSX_FIO)
      delete PSX_FIO;
   PSX_FIO = NULL;
   input_set_fio(NULL);

   DMA_Kill();

#ifdef HAVE_LIGHTREC
   MainRAM = NULL;
   ScratchRAM = NULL;
   BIOSROM = NULL;
   if(psx_mmap > 0)
      lightrec_free_mmap();
#else
   if(MainRAM)
      delete MainRAM;
   MainRAM = NULL;

   if(ScratchRAM)
      delete ScratchRAM;
   ScratchRAM = NULL;

   if(BIOSROM)
      delete BIOSROM;
   BIOSROM = NULL;
#endif

   if(PIOMem)
      delete PIOMem;
   PIOMem = NULL;

   cdifs = NULL;
}

static void CloseGame(void)
{
   int i;

   if (!failed_init)
   {
      for(i = 0; i < 8; i++)
      {
         if (i == 0 && !use_mednafen_memcard0_method)
         {
            PSX_FIO->SaveMemcard(i);
            continue;
         }

         // If there's an error saving one memcard, don't skip trying to save the other, since it might succeed and
         // we can reduce potential data loss!
         try
         {
            char ext[64];
            const char *memcard = NULL;
            if (i == 0)
               snprintf(ext, sizeof(ext), "%d.mcr", memcard_left_index);
            else if (i == 1)
               snprintf(ext, sizeof(ext), "%d.mcr", memcard_right_index);
            else
               snprintf(ext, sizeof(ext), "%d.mcr", i);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->SaveMemcard(i, memcard);
         }
         catch(std::exception &e)
         {
            log_cb(RETRO_LOG_ERROR, "%s\n", e.what());
         }
      }
   }

   Cleanup();
}

static void CDInsertEject(void)
{
   CD_TrayOpen = !CD_TrayOpen;

   for(unsigned disc = 0; disc < cdifs->size(); disc++)
   {
      if(!(*cdifs)[disc]->Eject(CD_TrayOpen))
      {
         MDFND_DispMessage(3, RETRO_LOG_ERROR,
               RETRO_MESSAGE_TARGET_ALL, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
               "Eject error.");

         CD_TrayOpen = !CD_TrayOpen;
      }
   }

   if(CD_TrayOpen)
      MDFND_DispMessage(0, RETRO_LOG_INFO,
            RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
            "Virtual CD Drive Tray Open");
   else
      MDFND_DispMessage(0, RETRO_LOG_INFO,
            RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
            "Virtual CD Drive Tray Closed");

   SetDiscWrapper(CD_TrayOpen);
}

static void CDEject(void)
{
   if(!CD_TrayOpen)
      CDInsertEject();
}

static void CDSelect(void)
{
   if(cdifs && CD_TrayOpen)
   {
      int disc_count = (CD_IsPBP ? PBP_PhysicalDiscCount : (int)cdifs->size());

      CD_SelectedDisc = (CD_SelectedDisc + 1) % (disc_count + 1);

      if(CD_SelectedDisc == disc_count)
         CD_SelectedDisc = -1;

      if(CD_SelectedDisc == -1)
         MDFND_DispMessage(0, RETRO_LOG_INFO,
               RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
               "Disc absence selected.");
      else
         MDFN_DispMessage(0, RETRO_LOG_INFO,
               RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
               "Disc %d of %d selected.", CD_SelectedDisc + 1, disc_count);
   }
}

extern "C" int StateAction(StateMem *sm, int load, int data_only)
{
   SFORMAT StateRegs[] =
   {
      SFVAR(CD_TrayOpen),
      SFVAR(CD_SelectedDisc),
      SFARRAYN(MainRAM->data8, 1024 * 2048, "MainRAM.data8"),
      SFARRAY32(SysControl.Regs, 9),
      SFVAR(PSX_PRNG.lcgo),
      SFVAR(PSX_PRNG.x),
      SFVAR(PSX_PRNG.y),
      SFVAR(PSX_PRNG.z),
      SFVAR(PSX_PRNG.c),
      SFEND
   };


   int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, "MAIN");

   // Call SetDisc() BEFORE we load CDC state, since SetDisc() has emulation side effects.  We might want to clean this up in the future.
   if(load)
   {
      if(CD_IsPBP)
      {
         if(!cdifs || CD_SelectedDisc >= PBP_PhysicalDiscCount)
            CD_SelectedDisc = -1;

         CDEject();
         CDInsertEject();
      } else {
         if(!cdifs || CD_SelectedDisc >= (int)cdifs->size())
            CD_SelectedDisc = -1;

         SetDiscWrapper(CD_TrayOpen);
      }
   }

   // TODO: Remember to increment dirty count in memory card state loading routine.

   ret &= PSX_CPU->StateAction(sm, load, data_only);
   ret &= DMA_StateAction(sm, load, data_only);
   ret &= TIMER_StateAction(sm, load, data_only);
   ret &= SIO_StateAction(sm, load, data_only);

   ret &= PSX_CDC->StateAction(sm, load, data_only);
   ret &= MDEC_StateAction(sm, load, data_only);
   ret &= GPU_StateAction(sm, load, data_only);
   ret &= PSX_SPU->StateAction(sm, load, data_only);

   ret &= PSX_FIO->StateAction(sm, load, data_only);

   ret &= IRQ_StateAction(sm, load, data_only); // Do it last.

   if(load)
   {
      ForceEventUpdates(0); // FIXME to work with debugger step mode.
   }

   return(ret);
}

static void DoSimpleCommand(int cmd)
{
   switch(cmd)
   {
      case MDFN_MSC_RESET:
         PSX_Power();
         break;
      case MDFN_MSC_POWER:
         PSX_Power();
         break;
      case MDFN_MSC_INSERT_DISK:
         CDInsertEject();
         break;
      case MDFN_MSC_SELECT_DISK:
         CDSelect();
         break;
      case MDFN_MSC_EJECT_DISK:
         CDEject();
         break;
   }
}

static void GSCondCode(MemoryPatch* patch, const char* cc, const unsigned len, const uint32 addr, const uint16 val)
{
   char tmp[256];

   if(patch->conditions.size() > 0)
      patch->conditions.append(", ");

   if(len == 2)
      snprintf(tmp, 256, "%u L 0x%08x %s 0x%04x", len, addr, cc, val & 0xFFFFU);
   else
      snprintf(tmp, 256, "%u L 0x%08x %s 0x%02x", len, addr, cc, val & 0xFFU);

   patch->conditions.append(tmp);
}

static bool DecodeGS(const std::string& cheat_string, MemoryPatch* patch)
{
   uint64 code = 0;
   unsigned nybble_count = 0;

   for(unsigned i = 0; i < cheat_string.size(); i++)
   {
      if(cheat_string[i] == ' ' || cheat_string[i] == '-' || cheat_string[i] == ':' || cheat_string[i] == '+')
         continue;

      nybble_count++;
      code <<= 4;

      if(cheat_string[i] >= '0' && cheat_string[i] <= '9')
         code |= cheat_string[i] - '0';
      else if(cheat_string[i] >= 'a' && cheat_string[i] <= 'f')
         code |= cheat_string[i] - 'a' + 0xA;
      else if(cheat_string[i] >= 'A' && cheat_string[i] <= 'F')
         code |= cheat_string[i] - 'A' + 0xA;
      else
      {
         if(cheat_string[i] & 0x80)
            log_cb(RETRO_LOG_ERROR, "[Mednafen]: Invalid character in GameShark code..\n");
         else
            log_cb(RETRO_LOG_ERROR, "[Mednafen]: Invalid character in GameShark code: %c.\n", cheat_string[i]);
         return false;
      }
   }

   if(nybble_count != 12)
   {
      log_cb(RETRO_LOG_ERROR, "GameShark code is of an incorrect length.\n");
      return false;
   }

   const uint8 code_type = code >> 40;
   const uint64 cl = code & 0xFFFFFFFFFFULL;

   patch->bigendian = false;
   patch->compare = 0;

   if(patch->type == 'T')
   {
      if(code_type != 0x80)
      log_cb(RETRO_LOG_ERROR, "Unrecognized GameShark code type for second part to copy bytes code.\n");

      patch->addr = cl >> 16;
      return(false);
   }

   switch(code_type)
   {
   default:
      log_cb(RETRO_LOG_ERROR, "GameShark code type 0x%02X is currently not supported.\n", code_type);
      return(false);

   // TODO:
   case 0x10:   // 16-bit increment
      patch->length = 2;
      patch->type = 'A';
      patch->addr = cl >> 16;
      patch->val = cl & 0xFFFF;
      return(false);

   case 0x11:   // 16-bit decrement
      patch->length = 2;
      patch->type = 'A';
      patch->addr = cl >> 16;
      patch->val = (0 - cl) & 0xFFFF;
      return(false);

   case 0x20:   // 8-bit increment
      patch->length = 1;
      patch->type = 'A';
      patch->addr = cl >> 16;
      patch->val = cl & 0xFF;
      return(false);

   case 0x21:   // 8-bit decrement
      patch->length = 1;
      patch->type = 'A';
      patch->addr = cl >> 16;
      patch->val = (0 - cl) & 0xFF;
      return(false);
   //
   //
   //

   case 0x30:   // 8-bit constant
      patch->length = 1;
      patch->type = 'R';
      patch->addr = cl >> 16;
      patch->val = cl & 0xFF;
      return(false);

   case 0x80:   // 16-bit constant
      patch->length = 2;
      patch->type = 'R';
      patch->addr = cl >> 16;
      patch->val = cl & 0xFFFF;
      return(false);

   case 0x50:   // Repeat thingy
   {
      const uint8 wcount = (cl >> 24) & 0xFF;
      const uint8 addr_inc = (cl >> 16) & 0xFF;
      const uint8 val_inc = (cl >> 0) & 0xFF;

      patch->mltpl_count = wcount;
      patch->mltpl_addr_inc = addr_inc;
      patch->mltpl_val_inc = val_inc;
   }
   return(true);

   case 0xC2:   // Copy
   {
      const uint16 ccount = cl & 0xFFFF;

      patch->type = 'T';
      patch->val = 0;
      patch->length = 1;

      patch->copy_src_addr = cl >> 16;
      patch->copy_src_addr_inc = 1;

      patch->mltpl_count = ccount;
      patch->mltpl_addr_inc = 1;
      patch->mltpl_val_inc = 0;
   }
   return(true);

  case 0xD0:   // 16-bit == condition
   GSCondCode(patch, "==", 2, cl >> 16, cl);
   return(true);

  case 0xD1:   // 16-bit != condition
   GSCondCode(patch, "!=", 2, cl >> 16, cl);
   return(true);

  case 0xD2:   // 16-bit < condition
   GSCondCode(patch, "<", 2, cl >> 16, cl);
   return(true);

  case 0xD3:   // 16-bit > condition
   GSCondCode(patch, ">", 2, cl >> 16, cl);
   return(true);



  case 0xE0:   // 8-bit == condition
   GSCondCode(patch, "==", 1, cl >> 16, cl);
   return(true);

  case 0xE1:   // 8-bit != condition
   GSCondCode(patch, "!=", 1, cl >> 16, cl);
   return(true);

  case 0xE2:   // 8-bit < condition
   GSCondCode(patch, "<", 1, cl >> 16, cl);
   return(true);

  case 0xE3:   // 8-bit > condition
   GSCondCode(patch, ">", 1, cl >> 16, cl);
   return(true);

 }
}

static CheatFormatStruct CheatFormats[] =
{
   { "GameShark", "Sharks with lamprey eels for eyes.", DecodeGS },
};

static CheatFormatInfoStruct CheatFormatInfo =
{
   1,
   CheatFormats
};

// Note for the future: If we ever support PSX emulation with non-8-bit RGB color components, or add a new linear RGB colorspace to MDFN_PixelFormat, we'll need
// to buffer the intermediate 24-bit non-linear RGB calculation into an array and pass that into the GPULineHook stuff, otherwise netplay could break when
// an emulated GunCon is used.  This IS assuming, of course, that we ever implement save state support so that netplay actually works at all...
MDFNGI EmulatedPSX =
{
   MDFN_MASTERCLOCK_FIXED(33868800),
   0,

   true, // Multires possible?

   //
   // Note: Following video settings will be overwritten during game load.
   //
   0,   // lcm_width
   0,   // lcm_height
   NULL,  // Dummy

   320,   // Nominal width
   240,   // Nominal height

   0,   // Framebuffer width
   0,   // Framebuffer height
   //
   //
   //

   2,     // Number of output sound channels

};

/* end of Mednafen psx.cpp */

//forward decls
extern void Emulate(EmulateSpecStruct *espec);

static bool overscan;
static double last_sound_rate;

#ifdef NEED_DEINTERLACER
static bool PrevInterlaced;
static Deinterlacer deint;
#endif

static MDFN_Surface *surf = NULL;

static void alloc_surface(void)
{
   MDFN_PixelFormat pix_fmt(MDFN_COLORSPACE_RGB, 16, 8, 0, 24);
   uint32_t width  = MEDNAFEN_CORE_GEOMETRY_MAX_W;
   uint32_t height = content_is_pal ? MEDNAFEN_CORE_GEOMETRY_MAX_H  : 480;

   width  <<= GPU_get_upscale_shift();
   height <<= GPU_get_upscale_shift();

   if (surf != NULL)
      delete surf;

   surf = new MDFN_Surface(NULL, width, height, width, pix_fmt);
}

static void check_system_specs(void)
{
   // Hints that we need a fairly powerful system to run this.
   unsigned level = 15;
   environ_cb(RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL, &level);
}

static unsigned disk_get_num_images(void)
{
   if(cdifs)
      return CD_IsPBP ? PBP_PhysicalDiscCount : cdifs->size();
   return 0;
}

static bool disk_set_eject_state(bool ejected)
{
   log_cb(RETRO_LOG_INFO, "[Mednafen]: Ejected: %u.\n", ejected);
   if (ejected == eject_state)
      return false;

   DoSimpleCommand(ejected ? MDFN_MSC_EJECT_DISK : MDFN_MSC_INSERT_DISK);
   eject_state = ejected;
   return true;
}

static bool disk_get_eject_state(void)
{
   return eject_state;
}

static unsigned disk_get_image_index(void)
{
   // PSX global. Hacky.
   return CD_SelectedDisc;
}

static bool disk_set_image_index(unsigned index)
{
   CD_SelectedDisc = index;
   if (CD_SelectedDisc > disk_get_num_images())
      CD_SelectedDisc = disk_get_num_images();

   // Very hacky. CDSelect command will increment first.
   CD_SelectedDisc--;

   DoSimpleCommand(MDFN_MSC_SELECT_DISK);
   return true;
}

// Mednafen PSX really doesn't support adding disk images on the fly ...
// Hack around this.

// Untested ...
static bool disk_replace_image_index(unsigned index, const struct retro_game_info *info)
{
   if (index >= disk_get_num_images() || !eject_state || CD_IsPBP)
      return false;

   if (!info)
   {
      delete cdifs->at(index);
      cdifs->erase(cdifs->begin() + index);
      if (index < CD_SelectedDisc)
         CD_SelectedDisc--;

      disk_control_ext_info.image_paths.erase(
            disk_control_ext_info.image_paths.begin() + index);
      disk_control_ext_info.image_labels.erase(
            disk_control_ext_info.image_labels.begin() + index);

      // Poke into psx.cpp
      CalcDiscSCEx();
      return true;
   }

   bool success = true;

   CDIF *iface = CDIF_Open(&success, info->path, false, false);

   if (!success)
      return false;

   delete cdifs->at(index);
   cdifs->at(index) = iface;
   CalcDiscSCEx();

   /* If we replace, we want the "swap disk manually effect". */
   extract_basename(retro_cd_base_name, info->path, sizeof(retro_cd_base_name));

   /* Update disk path/label vectors */
   disk_control_ext_info.image_paths[index]  = info->path;
   disk_control_ext_info.image_labels[index] = retro_cd_base_name;

   return true;
}

static bool disk_add_image_index(void)
{
   if(CD_IsPBP)
      return false;

   cdifs->push_back(NULL);
   disk_control_ext_info.image_paths.push_back("");
   disk_control_ext_info.image_labels.push_back("");
   return true;
}

static bool disk_set_initial_image(unsigned index, const char *path)
{
	if (string_is_empty(path))
		return false;

	disk_control_ext_info.initial_index = index;
	disk_control_ext_info.initial_path  = path;

	return true;
}

static bool disk_get_image_path(unsigned index, char *path, size_t len)
{
	if (len < 1)
		return false;

	if ((index < disk_get_num_images()) &&
		 (index < disk_control_ext_info.image_paths.size()))
	{
		if (!string_is_empty(disk_control_ext_info.image_paths[index].c_str()))
		{
			strlcpy(path, disk_control_ext_info.image_paths[index].c_str(), len);
			return true;
		}
	}

	return false;
}

static bool disk_get_image_label(unsigned index, char *label, size_t len)
{
	if (len < 1)
		return false;

	if ((index < disk_get_num_images()) &&
		 (index < disk_control_ext_info.image_labels.size()))
	{
		if (!string_is_empty(disk_control_ext_info.image_labels[index].c_str()))
		{
			strlcpy(label, disk_control_ext_info.image_labels[index].c_str(), len);
			return true;
		}
	}

	return false;
}

static struct retro_disk_control_callback disk_interface = {
   disk_set_eject_state,
   disk_get_eject_state,
   disk_get_image_index,
   disk_set_image_index,
   disk_get_num_images,
   disk_replace_image_index,
   disk_add_image_index,
};

static struct retro_disk_control_ext_callback disk_interface_ext =
{
	disk_set_eject_state,
	disk_get_eject_state,
	disk_get_image_index,
	disk_set_image_index,
	disk_get_num_images,
	disk_replace_image_index,
	disk_add_image_index,
	disk_set_initial_image,
	disk_get_image_path,
	disk_get_image_label,
};

static void fallback_log(enum retro_log_level level, const char *fmt, ...)
{
}


void retro_init(void)
{
   struct retro_log_callback log;
   uint64_t serialization_quirks = RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE;
   unsigned dci_version          = 0;

   if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log))
      log_cb = log.log;
   else
      log_cb = fallback_log;

   libretro_msg_interface_version = 0;
   environ_cb(RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION, &libretro_msg_interface_version);

   CDUtility_Init();

   eject_state = false;

   const char *dir = NULL;

   if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir) && dir)
   {
      snprintf(retro_base_directory, sizeof(retro_base_directory), "%s", dir);
   }
   else
   {
      /* TODO: Add proper fallback */
      log_cb(RETRO_LOG_WARN, "System directory is not defined. Fallback on using same dir as ROM for system directory later ...\n");
      failed_init = true;
   }

   if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir) && dir)
   {
      // If save directory is defined use it, otherwise use system directory
      if (dir)
         snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", dir);
      else
         snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", retro_base_directory);
   }
   else
   {
      /* TODO: Add proper fallback */
      log_cb(RETRO_LOG_WARN, "Save directory is not defined. Fallback on using SYSTEM directory ...\n");
      snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", retro_base_directory);
   }

   /* Initialise disk control interface */
   disk_control_ext_info.initial_index = 0;
   disk_control_ext_info.initial_path.clear();
   disk_control_ext_info.image_paths.clear();
   disk_control_ext_info.image_labels.clear();

   if (environ_cb(RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION, &dci_version) && (dci_version >= 1))
      environ_cb(RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE, &disk_interface_ext);
   else
      environ_cb(RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE, &disk_interface);

   if (environ_cb(RETRO_ENVIRONMENT_GET_PERF_INTERFACE, &perf_cb))
      perf_get_cpu_features_cb = perf_cb.get_cpu_features;
   else
      perf_get_cpu_features_cb = NULL;

   if (environ_cb(RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS, &serialization_quirks) &&
       (serialization_quirks & RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE))
      enable_variable_serialization_size = true;

   setting_initial_scanline = 0;
   setting_last_scanline = 239;
   setting_initial_scanline_pal = 0;
   setting_last_scanline_pal = 287;

   if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL))
      libretro_supports_bitmasks = true;

   check_system_specs();
}

void retro_reset(void)
{
   DoSimpleCommand(MDFN_MSC_RESET);
}

bool retro_load_game_special(unsigned, const struct retro_game_info *, size_t)
{
   return false;
}

#ifdef EMSCRIPTEN
static bool old_cdimagecache = true;
#else
static bool old_cdimagecache = false;
#endif

static bool boot = true;

// shared memory cards support
static bool shared_memorycards = false;

static bool has_new_geometry = false;
static bool has_new_timing = false;

static void check_variables(bool startup)
{
   struct retro_variable var = {0};

   extern void PSXDitherApply(bool);

#ifndef EMSCRIPTEN
   var.key = BEETLE_OPT(cd_access_method);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "sync") == 0)
      {
         old_cdimagecache = false;
         cd_async = false;
      }
      else if (strcmp(var.value, "async") == 0)
      {
         old_cdimagecache = false;
         cd_async = true;
      }
      else if (strcmp(var.value, "precache") == 0)
      {
         old_cdimagecache = true;
         cd_async = false;
      }
   }
#endif

#ifdef HAVE_LIGHTREC
   var.key = BEETLE_OPT(cpu_dynarec);

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "execute") == 0)
         psx_dynarec = DYNAREC_EXECUTE;
      else if (strcmp(var.value, "execute_one") == 0)
         psx_dynarec = DYNAREC_EXECUTE_ONE;
      else if (strcmp(var.value, "run_interpreter") == 0)
         psx_dynarec = DYNAREC_RUN_INTERPRETER;
      else
         psx_dynarec = DYNAREC_DISABLED;
   }
   else
      psx_dynarec = DYNAREC_DISABLED;

   var.key = BEETLE_OPT(dynarec_invalidate);

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "full") == 0)
         psx_dynarec_invalidate = false;
      else if (strcmp(var.value, "dma") == 0)
         psx_dynarec_invalidate = true;
   }
   else
      psx_dynarec_invalidate = false;

   var.key = BEETLE_OPT(dynarec_eventcycles);

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
	EventCycles = atoi(var.value);
   }
   else
      EventCycles = 128;
#endif

   var.key = BEETLE_OPT(cpu_freq_scale);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int scale_percent = atoi(var.value);

      if (scale_percent == 100)
         psx_overclock_factor = 0;
      else
         psx_overclock_factor = ((scale_percent << OVERCLOCK_SHIFT) + 50) / 100;
   }
   else
      psx_overclock_factor = 0;

   // Need to adjust the CPU<->GPU frequency ratio if the overclocking changes
   GPU_RecalcClockRatio();

   var.key = BEETLE_OPT(gte_overclock);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         psx_gte_overclock = true;
      else if (strcmp(var.value, "disabled") == 0)
         psx_gte_overclock = false;
   }
   else
      psx_gte_overclock = false;

   var.key = BEETLE_OPT(gpu_overclock);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         unsigned val = atoi(var.value);

         // Upscale must be a power of two
         assert((val & (val - 1)) == 0);

         // Crappy "ffs" implementation since the standard function is not
         // widely supported by libc in the wild
         uint8_t n;
         for (n = 0; (val & 1) == 0; ++n)
            {
               val >>= 1;
            }
         psx_gpu_overclock_shift = n;
      }
   else
      psx_gpu_overclock_shift = 0;

   var.key = BEETLE_OPT(skip_bios);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         psx_skipbios = 1;
      else
         psx_skipbios = 0;
   }

   var.key = BEETLE_OPT(widescreen_hack);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
      {
         if (widescreen_hack == false)
            has_new_geometry = true;
         widescreen_hack = true;
      }
      else if (strcmp(var.value, "disabled") == 0)
      {
         if (widescreen_hack == true)
            has_new_geometry = true;
         widescreen_hack = false;
      }
   }
   else
   {
      if (widescreen_hack == true)
         has_new_geometry = true;
      widescreen_hack = false;
   }

   var.key = BEETLE_OPT(pal_video_timing_override);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      bool want_fast_pal = (strcmp(var.value, "enabled") == 0);

      if (want_fast_pal != fast_pal) {
         fast_pal = want_fast_pal;
         has_new_timing = true;
      }
   }

   var.key = BEETLE_OPT(analog_calibration);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         input_enable_calibration(true);
      else if (strcmp(var.value, "disabled") == 0)
         input_enable_calibration(false);
   }
   else
      input_enable_calibration(false);

   var.key = BEETLE_OPT(core_timing_fps);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "force_progressive") == 0)
      {
         if (!startup && core_timing_fps_mode != FORCE_PROGRESSIVE_TIMING)
            has_new_timing = true;

         core_timing_fps_mode = FORCE_PROGRESSIVE_TIMING;
      }
      else if (strcmp(var.value, "force_interlaced") == 0)
      {
         if (!startup && core_timing_fps_mode != FORCE_INTERLACED_TIMING)
            has_new_timing = true;

         core_timing_fps_mode = FORCE_INTERLACED_TIMING;
      }
      else // auto toggle setting, timing changes are allowed
      {
         if (!startup && core_timing_fps_mode != AUTO_TOGGLE_TIMING)
            has_new_timing = true;

         core_timing_fps_mode = AUTO_TOGGLE_TIMING;
      }
   }

   var.key = BEETLE_OPT(aspect_ratio);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (!strcmp(var.value, "corrected"))
      {
         if (!startup && aspect_ratio_setting != 0)
            has_new_geometry = true;
         aspect_ratio_setting = 0;
      }
      else if (!strcmp(var.value, "uncorrected"))
      {
         if (!startup && aspect_ratio_setting != 1)
            has_new_geometry = true;
         aspect_ratio_setting = 1;
      }
      else if (!strcmp(var.value, "4:3"))
      {
         if (!startup && aspect_ratio_setting != 2)
            has_new_geometry = true;
         aspect_ratio_setting = 2;
      }
      else if (!strcmp(var.value, "ntsc"))
      {
         if (!startup && aspect_ratio_setting != 3)
            has_new_geometry = true;
         aspect_ratio_setting = 3;
      }
   }

   if (startup)
   {
      var.key = BEETLE_OPT(renderer);
      bool hw_renderer = false;
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         if (!strcmp(var.value, "hardware") || !strcmp(var.value, "hardware_gl") || !strcmp(var.value, "hardware_vk"))
         {
            hw_renderer = true;
         }
      }

      var.key = BEETLE_OPT(internal_resolution);
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         uint8_t new_upscale_shift;
         uint8_t val = atoi(var.value);

         // Upscale must be a power of two
         assert((val & (val - 1)) == 0);

         // Crappy "ffs" implementation since the standard function is not
         // widely supported by libc in the wild
         for (new_upscale_shift = 0; (val & 1) == 0; ++new_upscale_shift)
            val >>= 1;
         psx_gpu_upscale_shift_hw = new_upscale_shift;
      }
      else
         psx_gpu_upscale_shift_hw = 0;
      
      if (hw_renderer)
         psx_gpu_upscale_shift = 0;
      else
         psx_gpu_upscale_shift = psx_gpu_upscale_shift_hw;
   }
   else
   {
      rsx_intf_refresh_variables();

      var.key = BEETLE_OPT(internal_resolution);
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         uint8_t new_upscale_shift;
         uint8_t val = atoi(var.value);

         // Upscale must be a power of two
         assert((val & (val - 1)) == 0);

         // Crappy "ffs" implementation since the standard function is not
         // widely supported by libc in the wild
         for (new_upscale_shift = 0; (val & 1) == 0; ++new_upscale_shift)
            val >>= 1;
         psx_gpu_upscale_shift_hw = new_upscale_shift;
      }
      else
         psx_gpu_upscale_shift_hw = 0;

      switch (rsx_intf_is_type())
      {
         case RSX_SOFTWARE:
            psx_gpu_upscale_shift = psx_gpu_upscale_shift_hw;
            break;
         case RSX_OPENGL:
         case RSX_VULKAN:
            psx_gpu_upscale_shift = 0;
            break;
      }
   }

   var.key = BEETLE_OPT(dither_mode);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "1x(native)") == 0)
         psx_gpu_dither_mode = DITHER_NATIVE;
      else if (strcmp(var.value, "internal resolution") == 0)
         psx_gpu_dither_mode = DITHER_UPSCALED;
      else if (strcmp(var.value, "disabled") == 0)
         psx_gpu_dither_mode = DITHER_OFF;
   }
   else
      psx_gpu_dither_mode = DITHER_NATIVE;

   // iCB: PGXP settings
   var.key = BEETLE_OPT(pgxp_mode);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         psx_pgxp_mode = PGXP_MODE_NONE;
      else if (strcmp(var.value, "memory only") == 0)
         psx_pgxp_mode = PGXP_MODE_MEMORY | PGXP_MODE_GTE;
      else if (strcmp(var.value, "memory + CPU") == 0)
         psx_pgxp_mode = PGXP_MODE_MEMORY | PGXP_MODE_GTE | PGXP_MODE_CPU;
   }
   else
      psx_pgxp_mode = PGXP_MODE_NONE;

   var.key = BEETLE_OPT(pgxp_2d_tol);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         psx_pgxp_2d_tol = -1;
      else
         psx_pgxp_2d_tol = atoi(var.value);
   }
   else
      psx_pgxp_2d_tol = -1;

   var.key = BEETLE_OPT(pgxp_vertex);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         psx_pgxp_vertex_caching = PGXP_MODE_NONE;
      else if (strcmp(var.value, "enabled") == 0)
         psx_pgxp_vertex_caching = PGXP_VERTEX_CACHE;
   }
   else
      psx_pgxp_vertex_caching = PGXP_MODE_NONE;

   var.key = BEETLE_OPT(pgxp_texture);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         psx_pgxp_texture_correction = PGXP_MODE_NONE;
      else if (strcmp(var.value, "enabled") == 0)
         psx_pgxp_texture_correction = PGXP_TEXTURE_CORRECTION;
   }
   else
      psx_pgxp_texture_correction = PGXP_MODE_NONE;
   // \iCB

   var.key = BEETLE_OPT(pgxp_nclip);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         psx_pgxp_nclip = PGXP_MODE_NONE;
      else if (strcmp(var.value, "enabled") == 0)
         psx_pgxp_nclip = PGXP_NCLIP_IMPL;
   }
   else
      psx_pgxp_nclip = PGXP_MODE_NONE;

   var.key = BEETLE_OPT(line_render);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         line_render_mode = 0;
      else if (strcmp(var.value, "default") == 0)
         line_render_mode = 1;
      else if (strcmp(var.value, "aggressive") == 0)
         line_render_mode = 2;
   }

   var.key = BEETLE_OPT(filter);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int old_filter_mode = filter_mode;
      if (strcmp(var.value, "nearest") == 0)
         filter_mode = 0;
      else if (strcmp(var.value, "xBR") == 0)
         filter_mode = 1;
      else if (strcmp(var.value, "SABR") == 0)
         filter_mode = 2;
      else if (strcmp(var.value, "bilinear") == 0)
         filter_mode = 3;
      else if (strcmp(var.value, "3-point") == 0)
         filter_mode = 4;
      else if (strcmp(var.value, "JINC2") == 0)
         filter_mode = 5;

      if(filter_mode != old_filter_mode)
      {
         opaque_check = true;
         semitrans_check = true;
         old_filter_mode = filter_mode;
      }
   }

   var.key = BEETLE_OPT(analog_toggle);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if ((strcmp(var.value, "enabled") == 0)
            && setting_psx_analog_toggle != 1)
      {
         setting_psx_analog_toggle = 1;
         setting_apply_analog_toggle = true;
      }
      else if ((strcmp(var.value, "disabled") == 0)
            && setting_psx_analog_toggle != 0)
      {
         setting_psx_analog_toggle = 0;
         setting_apply_analog_toggle = true;
      }
   }

   var.key = BEETLE_OPT(enable_multitap_port1);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         setting_psx_multitap_port_1 = true;
      else if (strcmp(var.value, "disabled") == 0)
         setting_psx_multitap_port_1 = false;
   }

   var.key = BEETLE_OPT(enable_multitap_port2);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         setting_psx_multitap_port_2 = true;
      else if (strcmp(var.value, "disabled") == 0)
         setting_psx_multitap_port_2 = false;
   }

   var.key = BEETLE_OPT(mouse_sensitivity);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      input_set_mouse_sensitivity(atoi(var.value));

   var.key = BEETLE_OPT(gun_cursor);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "off") == 0)
         input_set_gun_cursor(FrontIO::SETTING_GUN_CROSSHAIR_OFF);
      else if (strcmp(var.value, "cross") == 0)
         input_set_gun_cursor(FrontIO::SETTING_GUN_CROSSHAIR_CROSS);
      else if (strcmp(var.value, "dot") == 0)
         input_set_gun_cursor(FrontIO::SETTING_GUN_CROSSHAIR_DOT);
   }

   var.key = BEETLE_OPT(gun_input_mode);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "lightgun") == 0)
         gun_input_mode = SETTING_GUN_INPUT_LIGHTGUN;
      else if (strcmp(var.value, "touchscreen") == 0)
         gun_input_mode = SETTING_GUN_INPUT_POINTER;
   }
   else
      gun_input_mode = SETTING_GUN_INPUT_LIGHTGUN;

   var.key = BEETLE_OPT(negcon_deadzone);
   input_set_negcon_deadzone(0);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      input_set_negcon_deadzone((int)(atoi(var.value) * 0.01f * NEGCON_RANGE));
   }

   var.key = BEETLE_OPT(negcon_response);
   input_set_negcon_linearity(1);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "quadratic") == 0)
         input_set_negcon_linearity(2);
      else if (strcmp(var.value, "cubic") == 0)
         input_set_negcon_linearity(3);
   }

   // Initial scanline NTSC
   var.key = BEETLE_OPT(initial_scanline);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int new_scanline_value = atoi(var.value);
      if (setting_initial_scanline != new_scanline_value)
      {
         has_new_geometry = true;
         setting_initial_scanline = new_scanline_value;
      }
   }

   // Last scanline NTSC
   var.key = BEETLE_OPT(last_scanline);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int new_scanline_value = atoi(var.value);
      if (setting_last_scanline != new_scanline_value)
      {
         has_new_geometry = true;
         setting_last_scanline = new_scanline_value;
      }
   }

   // Initial scanline PAL
   var.key = BEETLE_OPT(initial_scanline_pal);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int new_scanline_value = atoi(var.value);
      if (setting_initial_scanline_pal != new_scanline_value)
      {
         has_new_geometry = true;
         setting_initial_scanline_pal = new_scanline_value;
      }
   }

   // Last scanline PAL
   var.key = BEETLE_OPT(last_scanline_pal);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      int new_scanline_value = atoi(var.value);
      if (setting_last_scanline_pal != new_scanline_value)
      {
         has_new_geometry = true;
         setting_last_scanline_pal = new_scanline_value;
      }
   }

   if(setting_psx_multitap_port_1 && setting_psx_multitap_port_2)
      input_set_player_count(8);
   else if (setting_psx_multitap_port_1 || setting_psx_multitap_port_2)
      input_set_player_count(5);
   else
      input_set_player_count(2);

   /* Memcards (startup only) */
   if (startup)
   {
      var.key = BEETLE_OPT(use_mednafen_memcard0_method);
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         if (!strcmp(var.value, "libretro"))
            use_mednafen_memcard0_method = false;
         else if (!strcmp(var.value, "mednafen"))
            use_mednafen_memcard0_method = true;
      }

      var.key = BEETLE_OPT(enable_memcard1);
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         if (!strcmp(var.value, "enabled"))
            enable_memcard1 = true;
         else if (!strcmp(var.value, "disabled"))
            enable_memcard1 = false;
      }

      var.key = BEETLE_OPT(shared_memory_cards);
      if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
      {
         if (!strcmp(var.value, "enabled"))
         {
            if(use_mednafen_memcard0_method)
               shared_memorycards = true;
            else
               MDFND_DispMessage(3, RETRO_LOG_WARN,
                     RETRO_MESSAGE_TARGET_ALL, RETRO_MESSAGE_TYPE_NOTIFICATION,
                     "Memory Card 0 Method not set to Mednafen; shared memory cards could not be enabled.");
         }
         else if (!strcmp(var.value, "disabled"))
         {
            shared_memorycards = false;
         }
      }
   }
   /* End Memcards */

   var.key = BEETLE_OPT(frame_duping);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
      {
         bool can_dupe = false;
         if (environ_cb(RETRO_ENVIRONMENT_GET_CAN_DUPE, &can_dupe))
            allow_frame_duping = can_dupe;
      }
      else if (strcmp(var.value, "disabled") == 0)
         allow_frame_duping = false;
   }
   else
      allow_frame_duping = false;

   var.key = BEETLE_OPT(display_internal_fps);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
         display_internal_framerate = true;
      else if (strcmp(var.value, "disabled") == 0)
         display_internal_framerate = false;
   }
   else
      display_internal_framerate = false;

   var.key = BEETLE_OPT(crop_overscan);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "enabled") == 0)
      {
         if (crop_overscan == false)
            has_new_geometry = true;
         crop_overscan = true;
      }
      else if (strcmp(var.value, "disabled") == 0)
      {
         if (crop_overscan == true)
            has_new_geometry = true;
         crop_overscan = false;
      }
   }

   var.key = BEETLE_OPT(image_offset);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         image_offset = 0;
      else
         image_offset = atoi(var.value);
   }

   var.key = BEETLE_OPT(image_crop);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      if (strcmp(var.value, "disabled") == 0)
         image_crop = 0;
      else
         image_crop = atoi(var.value);
   }

   var.key = BEETLE_OPT(cd_fastload);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      uint8_t val = var.value[0] - '0';
      if (var.value[1] != 'x')
      {
         val  = (var.value[0] - '0') * 10;
         val += var.value[1] - '0';
      }
      // Value is a multiplier from the native 2x, so we divide by two
      cd_2x_speedup = val / 2;
   }
   else
      cd_2x_speedup = 1;

   var.key = BEETLE_OPT(memcard_left_index);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      memcard_left_index_old = memcard_left_index;
      memcard_left_index     = atoi(var.value);
   }

   var.key = BEETLE_OPT(memcard_right_index);
   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
   {
      memcard_right_index_old = memcard_right_index;
      memcard_right_index     = atoi(var.value);
   }
}

#ifdef NEED_CD
static void ReadM3U(std::vector<std::string> &file_list, std::string path, unsigned depth = 0)
{
   std::string dir_path;
   char linebuf[2048];
   FILE *fp = fopen(path.c_str(), "rb");

   if (fp == NULL)
      return;

   MDFN_GetFilePathComponents(path, &dir_path);

   while(fgets(linebuf, sizeof(linebuf), fp) != NULL)
   {
      std::string efp;

      if(linebuf[0] == '#')
         continue;
      string_trim_whitespace_right(linebuf);
      if(linebuf[0] == 0)
         continue;

      efp = MDFN_EvalFIP(dir_path, std::string(linebuf), false);

      if(efp.size() >= 4 && efp.substr(efp.size() - 4) == ".m3u")
      {
         if(efp == path)
         {
            log_cb(RETRO_LOG_ERROR, "M3U at \"%s\" references self.\n", efp.c_str());
            goto end;
         }

         if(depth == 99)
         {
            log_cb(RETRO_LOG_ERROR, "M3U load recursion too deep!\n");
            goto end;
         }

         ReadM3U(file_list, efp, depth++);
      }
      else
         file_list.push_back(efp);
   }

end:
   fclose(fp);
}

// TODO: LoadCommon()

static bool MDFNI_LoadCD(const char *devicename)
{
   log_cb(RETRO_LOG_INFO, "Loading %s...\n", devicename);

   try
   {
      if(devicename && strlen(devicename) > 4 && !strcasecmp(devicename + strlen(devicename) - 4, ".m3u"))
      {
         ReadM3U(disk_control_ext_info.image_paths, devicename);

         for(unsigned i = 0; i < disk_control_ext_info.image_paths.size(); i++)
         {
            char image_label[4096];
            bool success = true;

            image_label[0] = '\0';

            CDIF *image  = CDIF_Open(
                  &success, disk_control_ext_info.image_paths[i].c_str(), false, old_cdimagecache);
            CDInterfaces.push_back(image);

            extract_basename(
                  image_label, disk_control_ext_info.image_paths[i].c_str(),
                  sizeof(image_label));
            disk_control_ext_info.image_labels.push_back(image_label);
         }
      }
      else if(devicename && strlen(devicename) > 4 && !strcasecmp(devicename + strlen(devicename) - 4, ".pbp"))
      {
         bool success = true;
         CDIF *image  = CDIF_Open(&success, devicename, false, old_cdimagecache);
         CD_IsPBP     = true;
         CDInterfaces.push_back(image);

         /* CDIF_Open() sets PBP_DiscCount, so we can populate
          * image_paths/image_labels here */
         PBP_PhysicalDiscCount = (PBP_DiscCount == 0) ?
               1 : PBP_DiscCount;

         for(unsigned i = 0; i < PBP_PhysicalDiscCount; i++)
         {
            char image_name[4096];
            char image_label[4096];

            image_name[0]  = '\0';
            image_label[0] = '\0';

            /* All 'disks' have the same path when using
             * multi-disk PBP files */
            disk_control_ext_info.image_paths.push_back(devicename);

            /* Label is name+index */
            extract_basename(image_name, devicename, sizeof(image_name));
            snprintf(image_label, sizeof(image_label), "%s #%u", image_name, i + 1);
            disk_control_ext_info.image_labels.push_back(image_label);
         }
      }
      else
      {
         char image_label[4096];
         bool success = true;

         image_label[0] = '\0';

         CDIF *image  = CDIF_Open(&success, devicename, false, old_cdimagecache);
         if (!success)
            return false;

         CDInterfaces.push_back(image);

         disk_control_ext_info.image_paths.push_back(devicename);
         extract_basename(image_label, devicename, sizeof(image_label));
         disk_control_ext_info.image_labels.push_back(image_label);
      }
   }
   catch(std::exception &e)
   {
      log_cb(RETRO_LOG_ERROR, "Error opening CD.\n");
      return false;
   }

   // Print out a track list for all discs.
   for(unsigned i = 0; i < CDInterfaces.size(); i++)
   {
      TOC toc;
      TOC_Clear(&toc);

      CDInterfaces[i]->ReadTOC(&toc);

      log_cb(RETRO_LOG_DEBUG, "CD %d Layout:\n", i + 1);

      for(int32 track = toc.first_track; track <= toc.last_track; track++)
      {
         log_cb(RETRO_LOG_DEBUG, "Track %2d, LBA: %6d  %s\n", track, toc.tracks[track].lba, (toc.tracks[track].control & 0x4) ? "DATA" : "AUDIO");
      }

      log_cb(RETRO_LOG_DEBUG, "Leadout: %6d\n", toc.tracks[100].lba);
   }

   if(!(LoadCD(&CDInterfaces)))
   {
      for(unsigned i = 0; i < CDInterfaces.size(); i++)
         delete CDInterfaces[i];
      CDInterfaces.clear();

      disk_control_ext_info.initial_index = 0;
      disk_control_ext_info.initial_path.clear();
      disk_control_ext_info.image_paths.clear();
      disk_control_ext_info.image_labels.clear();

      return false;
   }

   //MDFNI_SetLayerEnableMask(~0ULL);

   MDFN_LoadGameCheats(NULL);
   MDFNMP_InstallReadPatches();

   return true;
}
#endif

static bool MDFNI_LoadGame(const char *name)
{
   RFILE *GameFile = NULL;

   if(strlen(name) > 4 && (
      !strcasecmp(name + strlen(name) - 4, ".cue") ||
      !strcasecmp(name + strlen(name) - 4, ".ccd") ||
      !strcasecmp(name + strlen(name) - 4, ".toc") ||
      !strcasecmp(name + strlen(name) - 4, ".m3u") ||
      !strcasecmp(name + strlen(name) - 4, ".chd") ||
      !strcasecmp(name + strlen(name) - 4, ".pbp")
      ))
    return MDFNI_LoadCD(name);

   GameFile = filestream_open(name,
         RETRO_VFS_FILE_ACCESS_READ,
         RETRO_VFS_FILE_ACCESS_HINT_NONE);

   if(!GameFile)
      goto error;

   if(Load(name, GameFile) <= 0)
      goto error;

   filestream_close(GameFile);
   GameFile   = NULL;

   return true;

error:
   if (GameFile)
      filestream_close(GameFile);
   GameFile     = NULL;

   return false;
}

bool retro_load_game(const struct retro_game_info *info)
{
   char tocbasepath[4096];

   if (failed_init)
      return false;

   input_init_env(environ_cb);

   enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
   if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
      return false;

   extract_basename(retro_cd_base_name,       info->path, sizeof(retro_cd_base_name));
   extract_directory(retro_cd_base_directory, info->path, sizeof(retro_cd_base_directory));

   int r = snprintf(tocbasepath, sizeof(tocbasepath), "%s%c%s.toc", retro_cd_base_directory, retro_slash, retro_cd_base_name);

   if (r >= 0 && r < 4096 && filestream_exists(tocbasepath))
      snprintf(retro_cd_path, sizeof(retro_cd_path), "%s", tocbasepath);
   else
      snprintf(retro_cd_path, sizeof(retro_cd_path), "%s", info->path);

   check_variables(true);

   if (!MDFNI_LoadGame(retro_cd_path))
   {
      failed_init = true;
      return false;
   }

   MDFN_LoadGameCheats(NULL);
   MDFNMP_InstallReadPatches();

   // Determine content_is_pal before calling alloc_surface()
   unsigned disc_region = CalcDiscSCEx();
   content_is_pal = (disc_region == REGION_EU);

   alloc_surface();

#ifdef NEED_DEINTERLACER
   PrevInterlaced = false;
   deint.ClearState();
#endif

   input_init();

   boot = false;

   frame_count = 0;
   internal_frame_count = 0;

   // MDFNI_LoadGame() has been called and surface has been allocated,
   // we can now perform firmware check
   bool force_software_renderer = false;
   if (!firmware_found)
   {
      log_cb(RETRO_LOG_ERROR, "Content cannot be loaded\n");

      /* TODO - We're forcing the sw renderer to show the ugui error message. Figure out
      how to copy the ugui framebuffer to the hardware renderer side with rsx_intf calls,
      so we don't have to force this anymore. */
      force_software_renderer = true;

#ifdef HAVE_LIGHTREC
      /* Do not run lightrec if firmware is not found, recompiling garbage is bad*/
      psx_dynarec = DYNAREC_DISABLED;
#endif
   }

   bool ret = rsx_intf_open(content_is_pal, force_software_renderer);

   /* Hide irrelevant core options */
   switch (rsx_intf_is_type())
   {
      case RSX_SOFTWARE:
      {
         struct retro_core_option_display option_display;
         option_display.visible = false;

         option_display.key = BEETLE_OPT(renderer_software_fb);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(scaled_uv_offset);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(filter_exclude_sprite);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(filter_exclude_2d_polygon);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(adaptive_smoothing);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(super_sampling);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(msaa);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(mdec_yuv);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(depth);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(wireframe);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(display_vram);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(filter);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(pgxp_vertex);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(pgxp_texture);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         option_display.key = BEETLE_OPT(image_offset_cycles);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         break;
      }
      case RSX_OPENGL:
      {
         struct retro_core_option_display option_display;
         option_display.visible = false;

         option_display.key = BEETLE_OPT(scaled_uv_offset);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(filter_exclude_sprite);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(filter_exclude_2d_polygon);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(adaptive_smoothing);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(super_sampling);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(msaa);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(mdec_yuv);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         option_display.key = BEETLE_OPT(image_offset);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(image_crop);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         option_display.key = BEETLE_OPT(frame_duping);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         break;
      }
      case RSX_VULKAN:
      {
         struct retro_core_option_display option_display;
         option_display.visible = false;

         option_display.key = BEETLE_OPT(depth);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(wireframe);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         option_display.key = BEETLE_OPT(image_offset);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
         option_display.key = BEETLE_OPT(image_crop);
         environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);

         break;
      }
   }

   /* Hide irrelevant scanline core options for current content */
   struct retro_core_option_display option_display;
   option_display.visible = false;
   if (content_is_pal)
   {
      option_display.key = BEETLE_OPT(initial_scanline);
      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
      option_display.key = BEETLE_OPT(last_scanline);
      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
   }
   else
   {
      option_display.key = BEETLE_OPT(initial_scanline_pal);
      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
      option_display.key = BEETLE_OPT(last_scanline_pal);
      environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
   }

   return ret;
}

void retro_unload_game(void)
{
   rsx_intf_close();

   MDFN_FlushGameCheats(0);

   CloseGame();

   MDFNMP_Kill();

   for(unsigned i = 0; i < CDInterfaces.size(); i++)
      delete CDInterfaces[i];
   CDInterfaces.clear();

   disk_control_ext_info.initial_index = 0;
   disk_control_ext_info.initial_path.clear();
   disk_control_ext_info.image_paths.clear();
   disk_control_ext_info.image_labels.clear();

   retro_cd_base_directory[0] = '\0';
   retro_cd_path[0]           = '\0';
   retro_cd_base_name[0]      = '\0';
}

static uint64_t video_frames, audio_frames;
#define SOUND_CHANNELS 2

void retro_run(void)
{
   bool updated = false;
   //code to implement audio and video disable is not yet implemented
   //bool disableVideo = false;
   //bool disableAudio = false;
   //bool hardDisableAudio = false;
   //int flags = 3;
   //if (environ_cb(RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE, &flags))
   //{
   //   disableVideo = !(flags & 1);
   //   disableAudio = !(flags & 2);
   //   hardDisableAudio = !!(flags & 8);
   //}

   if (gui_show && gui_inited && frame_width > 0 && frame_height > 0)
   {
      gui_draw();
      video_cb(gui_get_framebuffer(), frame_width, frame_height, frame_width * sizeof(unsigned));
   }

   rsx_intf_prepare_frame();

   if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
   {
      check_variables(false);
      struct retro_system_av_info new_av_info;

      /* Max width/height changed, need to call SET_SYSTEM_AV_INFO */
      if (GPU_get_upscale_shift() != psx_gpu_upscale_shift)
      {
         retro_get_system_av_info(&new_av_info);
         if (environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &new_av_info))
         {
            // We successfully changed the frontend's resolution, we can
            // apply the change immediately
            GPU_Rescale(psx_gpu_upscale_shift);
            alloc_surface();
            has_new_geometry = false;
         }
         else
         {
            // Failed, we have to postpone the upscaling change
            psx_gpu_upscale_shift = GPU_get_upscale_shift();
         }
      }

      /* Core timing option changed, need to call SET_SYSTEM_AV_INFO
       *
       * Note: May be possible to bundle this dirty flag with the other
       * dirty flags such as the one for widescreen hack and do a full
       * RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO callback; should be acceptable
       * to have video/audio reinits after changing core options
       */
      if (has_new_timing)
      {
         retro_get_system_av_info(&new_av_info);
         if (environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &new_av_info))
            has_new_timing = false;
      }

      /* Widescreen hack, scanlines, overscan cropping, or aspect ratio setting
         changed, need to call SET_GEOMETRY to change aspect ratio */
      if (has_new_geometry)
      {
         retro_get_system_av_info(&new_av_info);
         if (environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &new_av_info))
         {
            has_new_geometry = false;
         }
      }

      switch (psx_gpu_dither_mode)
      {
         case DITHER_NATIVE:
            GPU_set_dither_upscale_shift(psx_gpu_upscale_shift);
            break;
         case DITHER_UPSCALED:
            GPU_set_dither_upscale_shift(0);
            break;
         case DITHER_OFF:
            break;
      }

      GPU_set_visible_scanlines(MDFN_GetSettingI(content_is_pal ? "psx.slstartp" : "psx.slstart"),
                                MDFN_GetSettingI(content_is_pal ? "psx.slendp" : "psx.slend"));

      PGXP_SetModes(psx_pgxp_mode | psx_pgxp_vertex_caching | psx_pgxp_texture_correction | psx_pgxp_nclip);

      // Reload memory cards if they were changed
      if (use_mednafen_memcard0_method &&
          memcard_left_index_old != memcard_left_index)
      {
         MDFN_DispMessage(0, RETRO_LOG_INFO,
                          RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
                          "changing from memory card %d to memory card %d in left slot",
                          memcard_left_index_old, memcard_left_index);

         try
         {
            char ext[64];
            const char *memcard = NULL;

            // Save contents of left memory card to previously selected index
            snprintf(ext, sizeof(ext), "%d.mcr", memcard_left_index_old);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->SaveMemcard(0, memcard, true);

            // Load contents of currently selected index to left memory card
            snprintf(ext, sizeof(ext), "%d.mcr", memcard_left_index);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->LoadMemcard(0, memcard, true);
         }
         catch (std::exception &e)
         {
            log_cb(RETRO_LOG_ERROR, "%s\n", e.what());
         }
      }

      if (memcard_right_index_old != memcard_right_index)
      {
         MDFN_DispMessage(0, RETRO_LOG_INFO,
                          RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_NOTIFICATION_ALT,
                          "changing from memory card %d to memory card %d in right slot",
                          memcard_right_index_old, memcard_right_index);

         try
         {
            char ext[64];
            const char *memcard = NULL;

            // Save contents of right memory card to previously selected index
            snprintf(ext, sizeof(ext), "%d.mcr", memcard_right_index_old);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->SaveMemcard(1, memcard, true);

            // Load contents of currently selected index to right memory card
            snprintf(ext, sizeof(ext), "%d.mcr", memcard_right_index);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->LoadMemcard(1, memcard, true);
         }
         catch (std::exception &e)
         {
            log_cb(RETRO_LOG_ERROR, "%s\n", e.what());
         }
      }
   }

   /* We only start counting after the first frame we encounter. This
      way the value we display remains consistent if the real
      framerate is not a multiple of INTERNAL_FPS_SAMPLE_PERIOD
   */
   if (display_internal_framerate && internal_frame_count)
   {
      frame_count++;

      if (frame_count % INTERNAL_FPS_SAMPLE_PERIOD == 0)
      {
         char msg_buffer[64];

         msg_buffer[0] = '\0';

         // Just report the "real-world" refresh rate here regardless of system av info reported to the frontend
         float fps = (content_is_pal && !fast_pal) ?
                        (currently_interlaced ? FPS_PAL_INTERLACED : FPS_PAL_NONINTERLACED) :
                        (currently_interlaced ? FPS_NTSC_INTERLACED : FPS_NTSC_NONINTERLACED);
         float internal_fps = (internal_frame_count * fps) / INTERNAL_FPS_SAMPLE_PERIOD;

         snprintf(msg_buffer, sizeof(msg_buffer),
               "Internal FPS: %.2f", internal_fps);

         MDFND_DispMessage(1, RETRO_LOG_INFO,
               RETRO_MESSAGE_TARGET_OSD, RETRO_MESSAGE_TYPE_STATUS,
               msg_buffer);

         internal_frame_count = 0;
      }
   }
   else
   {
      // Keep the counters at 0 so that they don't display a bogus
      // value if this option is enabled later on
      frame_count = 0;
      internal_frame_count = 0;
   }

   if (setting_apply_analog_toggle)
   {
      PSX_FIO->SetAMCT(setting_psx_analog_toggle);
      setting_apply_analog_toggle = false;
   }

   input_poll_cb();

   input_update(libretro_supports_bitmasks, input_state_cb );

   static int32 rects[MEDNAFEN_CORE_GEOMETRY_MAX_H];
   rects[0] = ~0;

   EmulateSpecStruct spec = {0};
   spec.surface = surf;
   spec.SoundRate = 44100;
   spec.SoundBuf = NULL;
   spec.LineWidths = rects;
   spec.SoundBufMaxSize = 0;
   spec.SoundVolume = 1.0;
   spec.soundmultiplier = 1.0;
   spec.SoundBufSize = 0;
   spec.VideoFormatChanged = false;
   spec.SoundFormatChanged = false;

   //if (disableVideo)
   //{
   //   spec.skip = true;
   //}

   EmulateSpecStruct *espec = (EmulateSpecStruct*)&spec;
   /* start of Emulate */
   int32_t timestamp = 0;

   espec->skip = false;

   MDFNMP_ApplyPeriodicCheats();


   espec->MasterCycles = 0;
   espec->SoundBufSize = 0;

   PSX_FIO->UpdateInput();
   GPU_StartFrame(espec);

   Running = -1;
   timestamp = PSX_CPU->Run(timestamp, false, false);

   assert(timestamp);

   ForceEventUpdates(timestamp);
#if 0
   if(GPU_GetScanlineNum() < 100)
      PSX_DBG(PSX_DBG_ERROR, "[BUUUUUUUG] Frame timing end glitch; scanline=%u, st=%u\n", GPU_GetScanlineNum(), timestamp);
#endif

   //printf("scanline=%u, st=%u\n", GPU_GetScanlineNum(), timestamp);

   espec->SoundBufSize = IntermediateBufferPos;
   IntermediateBufferPos = 0;

   PSX_CDC->ResetTS();
   TIMER_ResetTS();
   DMA_ResetTS();
   GPU_ResetTS();
   PSX_FIO->ResetTS();

   RebaseTS(timestamp);

   espec->MasterCycles = timestamp;

   // Save memcards if dirty.
   unsigned players = input_get_player_count();
   for(int i = 0; i < players; i++)
   {
      uint64_t new_dc = PSX_FIO->GetMemcardDirtyCount(i);

      if(new_dc > Memcard_PrevDC[i])
      {
         Memcard_PrevDC[i] = new_dc;
         Memcard_SaveDelay[i] = 0;
      }

      if(Memcard_SaveDelay[i] >= 0)
      {
         Memcard_SaveDelay[i] += timestamp;
         if(Memcard_SaveDelay[i] >= (33868800 * 2))   // Wait until about 2 seconds of no new writes.
         {
            char ext[64];
            const char *memcard = NULL;

#ifndef NDEBUG
            log_cb(RETRO_LOG_INFO, "Saving memcard %d...\n", i);
#endif

            if (i == 0 && !use_mednafen_memcard0_method)
            {
               PSX_FIO->SaveMemcard(i);
               Memcard_SaveDelay[i] = -1;
               Memcard_PrevDC[i] = 0;
               continue;
            }

            int index = i;
            if (i == 0) index = memcard_left_index;
            else if (i == 1) index = memcard_right_index;

            snprintf(ext, sizeof(ext), "%d.mcr", index);
            memcard = MDFN_MakeFName(MDFNMKF_SAV, 0, ext);
            PSX_FIO->SaveMemcard(i, memcard);
            Memcard_SaveDelay[i] = -1;
            Memcard_PrevDC[i] = 0;
         }
      }
   }

   /* end of Emulate */

   // Check if aspect ratio needs to be changed due to display mode change on this frame
   if (MDFN_UNLIKELY((aspect_ratio_setting == 1) && aspect_ratio_dirty))
   {
      struct retro_system_av_info new_av_info;
      retro_get_system_av_info(&new_av_info);

      if (environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &new_av_info.geometry))
         aspect_ratio_dirty = false;

      // If unable to change geometry here, defer to next frame and leave aspect_ratio_dirty flagged
   }

   // Check if timing needs to be changed due to interlacing change on this frame
   // May be possible to track interlacing via espec instead of via RSX?
   if (MDFN_UNLIKELY((core_timing_fps_mode == AUTO_TOGGLE_TIMING) && interlace_setting_dirty))
   {
      // This may cause video and audio reinit on the frontend, so it may be preferable to
      // set the core option to force progressive or interlaced timings
      struct retro_system_av_info new_av_info;
      retro_get_system_av_info(&new_av_info);

      if (environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &new_av_info))
         interlace_setting_dirty = false;

      // If unable to change AV info here, defer to next frame and leave interlace_setting_dirty flagged
   }

   const void *fb        = NULL;
   unsigned width        = rects[0];
   unsigned height       = spec.DisplayRect.h;
   uint8_t upscale_shift = GPU_get_upscale_shift();

   if (rsx_intf_is_type() == RSX_SOFTWARE)
   {
#ifdef NEED_DEINTERLACER
      if (spec.InterlaceOn)
      {
         if (!PrevInterlaced)
            deint.ClearState();

         deint.Process(surf, spec.DisplayRect, rects, spec.InterlaceField);

         PrevInterlaced = true;

         spec.InterlaceOn = false;
         spec.InterlaceField = 0;
      }
      else
         PrevInterlaced = false;
#endif
      // PSX is rather special, and needs specific handling ...

      width = rects[0]; // spec.DisplayRect.w is 0. Only rects[0].w seems to return something sane.
      height = spec.DisplayRect.h;
      //fprintf(stderr, "(%u x %u)\n", width, height);

      // PSX core inserts padding on left and right (overscan). Optionally crop this.
      const uint32_t *pix = surf->pixels;
      unsigned pix_offset = 0;

      if (crop_overscan)
      {
         // Crop total # of pixels output by PSX in active scanline region down to # of pixels in corresponding horizontal display mode
         // 280 width -> 256 width.
         // 350 width -> 320 width.
         // 400 width -> 366 width.
         // 560 width -> 512 width.
         // 700 width -> 640 width.
         switch (width)
         {
            case 280:
               pix_offset += 12 - image_offset + floor(0.5 * image_crop);
               width = 256 - image_crop;
               break;

            case 350:
               pix_offset += 15 - image_offset + floor(0.5 * image_crop);
               width = 320 - image_crop;
               break;

            /* 368px mode. Some games are overcropped at 364 width or undercropped at 368 width, so crop to 366.
               Adjust in future if there are issues. */
            case 400:
               pix_offset += 17 - image_offset + floor(0.5 * image_crop);
               width = 366 - image_crop;
               break;

            case 560:
               pix_offset += 24 - image_offset + floor(0.5 * image_crop);
               width = 512 - image_crop;
               break;

            case 700:
               pix_offset += 30 - image_offset + floor(0.5 * image_crop);
               width = 640 - image_crop;
               break;

            default:
               // This shouldn't happen.
               break;
         }
      }


      width  <<= upscale_shift;
      height <<= upscale_shift;
      pix     += pix_offset << upscale_shift;

      if (GPU_get_display_possibly_dirty() || (GPU_get_display_change_count() != 0) || !allow_frame_duping)
         fb = pix;
   }

   int16_t *interbuf = (int16_t*)&IntermediateBuffer;

   if (gui_show)
   {
      if (!gui_inited)
      {
         frame_width = width;
         frame_height = height;

         gui_init(frame_width, frame_height, sizeof(unsigned));
         gui_set_window_title("Error");
         gui_inited = true;
      }

      if (width != frame_width || height != frame_height)
      {
        frame_width = width;
        frame_height = height;
        gui_window_resize(0, 0, frame_width, frame_height);
      }
   }
   else
   {
      rsx_intf_finalize_frame(fb, width, height,
            MEDNAFEN_CORE_GEOMETRY_MAX_W << (2 + upscale_shift));
   }

   video_frames++;
   audio_frames += spec.SoundBufSize;

   audio_batch_cb(interbuf, spec.SoundBufSize);

   if (GPU_get_display_possibly_dirty() || (GPU_get_display_change_count() != 0))
   {
      internal_frame_count++;
      GPU_set_display_change_count(0);
      GPU_set_display_possibly_dirty(false);
   }
}

void retro_get_system_info(struct retro_system_info *info)
{
   memset(info, 0, sizeof(*info));
   info->library_name     = MEDNAFEN_CORE_NAME;
#ifdef GIT_VERSION
   info->library_version  = MEDNAFEN_CORE_VERSION GIT_VERSION;
#else
   info->library_version  = MEDNAFEN_CORE_VERSION;
#endif
   info->need_fullpath    = true;
   info->valid_extensions = MEDNAFEN_CORE_EXTENSIONS;
   info->block_extract    = false;
}

void retro_get_system_av_info(struct retro_system_av_info *info)
{
   rsx_intf_get_system_av_info(info);
}

void retro_deinit(void)
{
   delete surf;
   surf = NULL;

   log_cb(RETRO_LOG_DEBUG, "[%s]: Samples / Frame: %.5f\n",
         MEDNAFEN_CORE_NAME, (double)audio_frames / video_frames);
   log_cb(RETRO_LOG_DEBUG, "[%s]: Estimated FPS: %.5f\n",
         MEDNAFEN_CORE_NAME, (double)video_frames * 44100 / audio_frames);

   libretro_supports_bitmasks = false;
}

unsigned retro_get_region(void)
{
   // simias: should I override this when fast_pal is set?
   //
   // I'm not entirely sure what's that used for.
   return content_is_pal ? RETRO_REGION_PAL : RETRO_REGION_NTSC;
}

unsigned retro_api_version(void)
{
   return RETRO_API_VERSION;
}

#include "libretro_core_options.h"

void retro_set_environment(retro_environment_t cb)
{
   struct retro_vfs_interface_info vfs_iface_info;
   environ_cb = cb;

   libretro_set_core_options(environ_cb);

   vfs_iface_info.required_interface_version = 1;
   vfs_iface_info.iface                      = NULL;
   if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs_iface_info))
	   filestream_vfs_init(&vfs_iface_info);

	input_set_env( cb );

   rsx_intf_set_environment(cb);
}

void retro_set_audio_sample(retro_audio_sample_t cb)
{
   audio_cb = cb;
}

void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
{
   audio_batch_cb = cb;
}

void retro_set_input_poll(retro_input_poll_t cb)
{
   input_poll_cb = cb;
}

void retro_set_input_state(retro_input_state_t cb)
{
   input_state_cb = cb;
   dbg_input_state_cb = cb;
}

void retro_set_video_refresh(retro_video_refresh_t cb)
{
   video_cb = cb;

   rsx_intf_set_video_refresh(cb);
}

size_t retro_serialize_size(void)
{
   if (enable_variable_serialization_size)
   {
      StateMem st;

      st.data           = NULL;
      st.loc            = 0;
      st.len            = 0;
      st.malloced       = 0;
      st.initial_malloc = 0;

      if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
         return 0;

      free(st.data);
      return st.len;
   }

   return DEFAULT_STATE_SIZE; // 16MB
}

bool UsingFastSavestates(void)
{
   int flags;
   if (environ_cb(RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE, &flags))
      return flags & 4;
   return false;
}

bool retro_serialize(void *data, size_t size)
{
   StateMem st;
   bool ret          = false;

   st.len            = 0;
   st.loc            = 0;
   st.malloced       = size;
   st.initial_malloc = 0;

   if (size == DEFAULT_STATE_SIZE)  //16MB buffer reserved
   {
      //actual size is around 3.75MB (3.67MB for fast savestates) rather than 16MB, so 16MB will hold a savestate without worrying about realloc

      //save state in place
      st.data     = (uint8_t*)data;

      //fast save states are at least 20% faster
      FastSaveStates = UsingFastSavestates();
      ret = MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL);
   }
   else
   {
      /* it seems that mednafen can realloc pointers sent to it?
         since we don't know the disposition of void* data (is it safe to realloc?) we have to manage a new buffer here */
      static bool logged;
      uint8_t *_dat = (uint8_t*)malloc(size);

      if (!_dat)
         return false;

      st.data = _dat;

      /* there are still some errors with the save states,
       * the size seems to change on some games for now
       * just log when this happens */
      if (!logged && st.len != size)
      {
         log_cb(RETRO_LOG_WARN, "warning, save state size has changed\n");
         logged = true;
      }

      FastSaveStates = UsingFastSavestates();
      ret = MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL);

      memcpy(data, st.data, size);
      free(st.data);
   }

   FastSaveStates = false;

   return ret;
}

bool retro_unserialize(const void *data, size_t size)
{
   StateMem st;

   st.data           = (uint8_t*)data;
   st.loc            = 0;
   st.len            = size;
   st.malloced       = 0;
   st.initial_malloc = 0;

   //fast save states are at least 20% faster
   FastSaveStates = UsingFastSavestates();
   bool okay = MDFNSS_LoadSM(&st, 0, 0);
   FastSaveStates = false;
   return okay;
}

void *retro_get_memory_data(unsigned type)
{
   uint8_t *data;

   switch (type)
   {
      case RETRO_MEMORY_SYSTEM_RAM:
         return MainRAM->data8;
      case RETRO_MEMORY_SAVE_RAM:
         if (!use_mednafen_memcard0_method)
            return PSX_FIO->GetMemcardDevice(0)->GetNVData();
         break;
      default:
         break;
   }
   return NULL;
}

size_t retro_get_memory_size(unsigned type)
{
   switch (type)
   {
      case RETRO_MEMORY_SYSTEM_RAM:
         return 0x200000;
      case RETRO_MEMORY_SAVE_RAM:
         if (!use_mednafen_memcard0_method)
            return (1 << 17);
         break;
      default:
         break;
   }

   return 0;
}

void retro_cheat_reset(void)
{
   MDFN_FlushGameCheats(1);
}

void retro_cheat_set(unsigned index, bool enabled, const char * codeLine)
{
   const CheatFormatStruct* cf = CheatFormats;
   char name[256];
   std::vector<std::string> codeParts;
   int matchLength=0;
   int cursor;
   std::string part;

   if (codeLine==NULL)
      return;

   //Break the code into Parts
   for (cursor=0;;cursor++)
   {
      if (ISHEXDEC)
         matchLength++;
      else
      {
         if (matchLength)
         {
            part=codeLine+cursor-matchLength;
            part.erase(matchLength,std::string::npos);
            codeParts.push_back(part);
            matchLength=0;
         }
      }
      if (!codeLine[cursor])
         break;
   }

   MemoryPatch patch;
   bool trueML=0;
   for (cursor=0;cursor<codeParts.size();cursor++)
   {
      part=codeParts[cursor];
      if (part.length()==8)
         part+=codeParts[++cursor];
      if (part.length()==12)
      {
         //Decode the cheat
         try
         {
            if(!cf->DecodeCheat(std::string(part), &patch))
            {
               //Generate a name
               sprintf(name,"cheat_%i_%i",index,cursor);

               //Set parameters
               patch.name=(std::string)name;
               patch.status=enabled;

               MDFNI_AddCheat(patch);
               patch=MemoryPatch();
            }
         }
         catch(std::exception &e)
         {
             continue;
         }
      }
   }
}

// Use a simpler approach to make sure that things go right for libretro.
const char *MDFN_MakeFName(MakeFName_Type type, int id1, const char *cd1)
{
   static char fullpath[4096];
   int r = 0;

   fullpath[0] = '\0';

   switch (type)
   {
      case MDFNMKF_SAV:
         r = snprintf(fullpath, sizeof(fullpath), "%s%c%s.%s",
               retro_save_directory,
               retro_slash,
               shared_memorycards ? "mednafen_psx_libretro_shared" : retro_cd_base_name,
               cd1);
         break;
      case MDFNMKF_FIRMWARE:
         r = snprintf(fullpath, sizeof(fullpath), "%s%c%s", retro_base_directory, retro_slash, cd1);
         break;
      default:
         break;
   }

   if (r > 4095)
   {
      log_cb(RETRO_LOG_ERROR,"MakeFName path longer than 4095\n");
      fullpath[4095] = '\0';
   }

   return fullpath;
}

void MDFND_DispMessage(
      unsigned priority, enum retro_log_level level,
      enum retro_message_target target, enum retro_message_type type,
      const char *str)
{
   if (libretro_msg_interface_version >= 1)
   {
      struct retro_message_ext msg = {
         str,
         3000,
         priority,
         level,
         target,
         type,
         -1
      };
      environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE_EXT, &msg);
   }
   else
   {
      struct retro_message msg =
      {
         str,
         180
      };
      environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
   }
}

void MDFN_DispMessage(
      unsigned priority, enum retro_log_level level,
      enum retro_message_target target, enum retro_message_type type,
      const char *format, ...)
{
   va_list ap;
   char *str = (char*)malloc(4096 * sizeof(char));

   va_start(ap, format);
   vsnprintf(str, 4096, format, ap);
   va_end(ap);

   MDFND_DispMessage(priority, level, target, type, str);
   free(str);
}
